Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

555 lines
16KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2013 - Raw Material Software Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. TabBarButton::TabBarButton (const String& name, TabbedButtonBar& owner_)
  18. : Button (name), owner (owner_), overlapPixels (0), extraCompPlacement (afterText)
  19. {
  20. setWantsKeyboardFocus (false);
  21. }
  22. TabBarButton::~TabBarButton() {}
  23. int TabBarButton::getIndex() const { return owner.indexOfTabButton (this); }
  24. Colour TabBarButton::getTabBackgroundColour() const { return owner.getTabBackgroundColour (getIndex()); }
  25. bool TabBarButton::isFrontTab() const { return getToggleState(); }
  26. void TabBarButton::paintButton (Graphics& g, const bool isMouseOverButton, const bool isButtonDown)
  27. {
  28. getLookAndFeel().drawTabButton (*this, g, isMouseOverButton, isButtonDown);
  29. }
  30. void TabBarButton::clicked (const ModifierKeys& mods)
  31. {
  32. if (mods.isPopupMenu())
  33. owner.popupMenuClickOnTab (getIndex(), getButtonText());
  34. else
  35. owner.setCurrentTabIndex (getIndex());
  36. }
  37. bool TabBarButton::hitTest (int mx, int my)
  38. {
  39. const Rectangle<int> area (getActiveArea());
  40. if (owner.isVertical())
  41. {
  42. if (isPositiveAndBelow (mx, getWidth())
  43. && my >= area.getY() + overlapPixels && my < area.getBottom() - overlapPixels)
  44. return true;
  45. }
  46. else
  47. {
  48. if (isPositiveAndBelow (my, getHeight())
  49. && mx >= area.getX() + overlapPixels && mx < area.getRight() - overlapPixels)
  50. return true;
  51. }
  52. Path p;
  53. getLookAndFeel().createTabButtonShape (*this, p, false, false);
  54. return p.contains ((float) (mx - area.getX()),
  55. (float) (my - area.getY()));
  56. }
  57. int TabBarButton::getBestTabLength (const int depth)
  58. {
  59. return getLookAndFeel().getTabButtonBestWidth (*this, depth);
  60. }
  61. void TabBarButton::calcAreas (Rectangle<int>& extraComp, Rectangle<int>& textArea) const
  62. {
  63. LookAndFeel& lf = getLookAndFeel();
  64. textArea = getActiveArea();
  65. const int depth = owner.isVertical() ? textArea.getWidth() : textArea.getHeight();
  66. const int overlap = lf.getTabButtonOverlap (depth);
  67. if (overlap > 0)
  68. {
  69. if (owner.isVertical())
  70. textArea.reduce (0, overlap);
  71. else
  72. textArea.reduce (overlap, 0);
  73. }
  74. if (extraComponent != nullptr)
  75. {
  76. extraComp = lf.getTabButtonExtraComponentBounds (*this, textArea, *extraComponent);
  77. const TabbedButtonBar::Orientation orientation = owner.getOrientation();
  78. if (orientation == TabbedButtonBar::TabsAtLeft || orientation == TabbedButtonBar::TabsAtRight)
  79. {
  80. if (extraComp.getCentreY() > textArea.getCentreY())
  81. textArea.setBottom (jmin (textArea.getBottom(), extraComp.getY()));
  82. else
  83. textArea.setTop (jmax (textArea.getY(), extraComp.getBottom()));
  84. }
  85. else
  86. {
  87. if (extraComp.getCentreX() > textArea.getCentreX())
  88. textArea.setRight (jmin (textArea.getRight(), extraComp.getX()));
  89. else
  90. textArea.setLeft (jmax (textArea.getX(), extraComp.getRight()));
  91. }
  92. }
  93. }
  94. Rectangle<int> TabBarButton::getTextArea() const
  95. {
  96. Rectangle<int> extraComp, textArea;
  97. calcAreas (extraComp, textArea);
  98. return textArea;
  99. }
  100. Rectangle<int> TabBarButton::getActiveArea() const
  101. {
  102. Rectangle<int> r (getLocalBounds());
  103. const int spaceAroundImage = getLookAndFeel().getTabButtonSpaceAroundImage();
  104. const TabbedButtonBar::Orientation orientation = owner.getOrientation();
  105. if (orientation != TabbedButtonBar::TabsAtLeft) r.removeFromRight (spaceAroundImage);
  106. if (orientation != TabbedButtonBar::TabsAtRight) r.removeFromLeft (spaceAroundImage);
  107. if (orientation != TabbedButtonBar::TabsAtBottom) r.removeFromTop (spaceAroundImage);
  108. if (orientation != TabbedButtonBar::TabsAtTop) r.removeFromBottom (spaceAroundImage);
  109. return r;
  110. }
  111. void TabBarButton::setExtraComponent (Component* comp, ExtraComponentPlacement placement)
  112. {
  113. jassert (extraCompPlacement == beforeText || extraCompPlacement == afterText);
  114. extraCompPlacement = placement;
  115. addAndMakeVisible (extraComponent = comp);
  116. resized();
  117. }
  118. void TabBarButton::childBoundsChanged (Component* c)
  119. {
  120. if (c == extraComponent)
  121. {
  122. owner.resized();
  123. resized();
  124. }
  125. }
  126. void TabBarButton::resized()
  127. {
  128. if (extraComponent != nullptr)
  129. {
  130. Rectangle<int> extraComp, textArea;
  131. calcAreas (extraComp, textArea);
  132. if (! extraComp.isEmpty())
  133. extraComponent->setBounds (extraComp);
  134. }
  135. }
  136. //==============================================================================
  137. class TabbedButtonBar::BehindFrontTabComp : public Component,
  138. public ButtonListener // (can't use Button::Listener due to idiotic VC2005 bug)
  139. {
  140. public:
  141. BehindFrontTabComp (TabbedButtonBar& tb) : owner (tb)
  142. {
  143. setInterceptsMouseClicks (false, false);
  144. }
  145. void paint (Graphics& g) override
  146. {
  147. getLookAndFeel().drawTabAreaBehindFrontButton (owner, g, getWidth(), getHeight());
  148. }
  149. void enablementChanged() override
  150. {
  151. repaint();
  152. }
  153. void buttonClicked (Button*) override
  154. {
  155. owner.showExtraItemsMenu();
  156. }
  157. private:
  158. TabbedButtonBar& owner;
  159. JUCE_DECLARE_NON_COPYABLE (BehindFrontTabComp)
  160. };
  161. //==============================================================================
  162. TabbedButtonBar::TabbedButtonBar (const Orientation orientation_)
  163. : orientation (orientation_),
  164. minimumScale (0.7),
  165. currentTabIndex (-1)
  166. {
  167. setInterceptsMouseClicks (false, true);
  168. addAndMakeVisible (behindFrontTab = new BehindFrontTabComp (*this));
  169. setFocusContainer (true);
  170. }
  171. TabbedButtonBar::~TabbedButtonBar()
  172. {
  173. tabs.clear();
  174. extraTabsButton = nullptr;
  175. }
  176. //==============================================================================
  177. void TabbedButtonBar::setOrientation (const Orientation newOrientation)
  178. {
  179. orientation = newOrientation;
  180. for (int i = getNumChildComponents(); --i >= 0;)
  181. getChildComponent (i)->resized();
  182. resized();
  183. }
  184. TabBarButton* TabbedButtonBar::createTabButton (const String& name, const int /*index*/)
  185. {
  186. return new TabBarButton (name, *this);
  187. }
  188. void TabbedButtonBar::setMinimumTabScaleFactor (double newMinimumScale)
  189. {
  190. minimumScale = newMinimumScale;
  191. resized();
  192. }
  193. //==============================================================================
  194. void TabbedButtonBar::clearTabs()
  195. {
  196. tabs.clear();
  197. extraTabsButton = nullptr;
  198. setCurrentTabIndex (-1);
  199. }
  200. void TabbedButtonBar::addTab (const String& tabName,
  201. Colour tabBackgroundColour,
  202. int insertIndex)
  203. {
  204. jassert (tabName.isNotEmpty()); // you have to give them all a name..
  205. if (tabName.isNotEmpty())
  206. {
  207. if (! isPositiveAndBelow (insertIndex, tabs.size()))
  208. insertIndex = tabs.size();
  209. TabInfo* const currentTab = tabs [currentTabIndex];
  210. TabInfo* newTab = new TabInfo();
  211. newTab->name = tabName;
  212. newTab->colour = tabBackgroundColour;
  213. newTab->button = createTabButton (tabName, insertIndex);
  214. jassert (newTab->button != nullptr);
  215. tabs.insert (insertIndex, newTab);
  216. currentTabIndex = tabs.indexOf (currentTab);
  217. addAndMakeVisible (newTab->button, insertIndex);
  218. resized();
  219. if (currentTabIndex < 0)
  220. setCurrentTabIndex (0);
  221. }
  222. }
  223. void TabbedButtonBar::setTabName (const int tabIndex, const String& newName)
  224. {
  225. if (TabInfo* const tab = tabs [tabIndex])
  226. {
  227. if (tab->name != newName)
  228. {
  229. tab->name = newName;
  230. tab->button->setButtonText (newName);
  231. resized();
  232. }
  233. }
  234. }
  235. void TabbedButtonBar::removeTab (const int tabIndex)
  236. {
  237. const int oldIndex = currentTabIndex;
  238. if (tabIndex == currentTabIndex)
  239. setCurrentTabIndex (-1);
  240. tabs.remove (tabIndex);
  241. setCurrentTabIndex (oldIndex);
  242. resized();
  243. }
  244. void TabbedButtonBar::moveTab (const int currentIndex, const int newIndex)
  245. {
  246. TabInfo* const currentTab = tabs [currentTabIndex];
  247. tabs.move (currentIndex, newIndex);
  248. currentTabIndex = tabs.indexOf (currentTab);
  249. resized();
  250. }
  251. int TabbedButtonBar::getNumTabs() const
  252. {
  253. return tabs.size();
  254. }
  255. String TabbedButtonBar::getCurrentTabName() const
  256. {
  257. TabInfo* tab = tabs [currentTabIndex];
  258. return tab == nullptr ? String::empty : tab->name;
  259. }
  260. StringArray TabbedButtonBar::getTabNames() const
  261. {
  262. StringArray names;
  263. for (int i = 0; i < tabs.size(); ++i)
  264. names.add (tabs.getUnchecked(i)->name);
  265. return names;
  266. }
  267. void TabbedButtonBar::setCurrentTabIndex (int newIndex, const bool sendChangeMessage_)
  268. {
  269. if (currentTabIndex != newIndex)
  270. {
  271. if (! isPositiveAndBelow (newIndex, tabs.size()))
  272. newIndex = -1;
  273. currentTabIndex = newIndex;
  274. for (int i = 0; i < tabs.size(); ++i)
  275. {
  276. TabBarButton* tb = tabs.getUnchecked(i)->button;
  277. tb->setToggleState (i == newIndex, dontSendNotification);
  278. }
  279. resized();
  280. if (sendChangeMessage_)
  281. sendChangeMessage();
  282. currentTabChanged (newIndex, getCurrentTabName());
  283. }
  284. }
  285. TabBarButton* TabbedButtonBar::getTabButton (const int index) const
  286. {
  287. if (TabInfo* tab = tabs[index])
  288. return static_cast<TabBarButton*> (tab->button);
  289. return nullptr;
  290. }
  291. int TabbedButtonBar::indexOfTabButton (const TabBarButton* button) const
  292. {
  293. for (int i = tabs.size(); --i >= 0;)
  294. if (tabs.getUnchecked(i)->button == button)
  295. return i;
  296. return -1;
  297. }
  298. void TabbedButtonBar::lookAndFeelChanged()
  299. {
  300. extraTabsButton = nullptr;
  301. resized();
  302. }
  303. void TabbedButtonBar::paint (Graphics& g)
  304. {
  305. getLookAndFeel().drawTabbedButtonBarBackground (*this, g);
  306. }
  307. void TabbedButtonBar::resized()
  308. {
  309. LookAndFeel& lf = getLookAndFeel();
  310. int depth = getWidth();
  311. int length = getHeight();
  312. if (! isVertical())
  313. std::swap (depth, length);
  314. const int overlap = lf.getTabButtonOverlap (depth) + lf.getTabButtonSpaceAroundImage() * 2;
  315. int totalLength = jmax (0, overlap);
  316. int numVisibleButtons = tabs.size();
  317. for (int i = 0; i < tabs.size(); ++i)
  318. {
  319. TabBarButton* const tb = tabs.getUnchecked(i)->button;
  320. totalLength += tb->getBestTabLength (depth) - overlap;
  321. tb->overlapPixels = jmax (0, overlap / 2);
  322. }
  323. double scale = 1.0;
  324. if (totalLength > length)
  325. scale = jmax (minimumScale, length / (double) totalLength);
  326. const bool isTooBig = (int) (totalLength * scale) > length;
  327. int tabsButtonPos = 0;
  328. if (isTooBig)
  329. {
  330. if (extraTabsButton == nullptr)
  331. {
  332. addAndMakeVisible (extraTabsButton = lf.createTabBarExtrasButton());
  333. extraTabsButton->addListener (behindFrontTab);
  334. extraTabsButton->setAlwaysOnTop (true);
  335. extraTabsButton->setTriggeredOnMouseDown (true);
  336. }
  337. const int buttonSize = jmin (proportionOfWidth (0.7f), proportionOfHeight (0.7f));
  338. extraTabsButton->setSize (buttonSize, buttonSize);
  339. if (isVertical())
  340. {
  341. tabsButtonPos = getHeight() - buttonSize / 2 - 1;
  342. extraTabsButton->setCentrePosition (getWidth() / 2, tabsButtonPos);
  343. }
  344. else
  345. {
  346. tabsButtonPos = getWidth() - buttonSize / 2 - 1;
  347. extraTabsButton->setCentrePosition (tabsButtonPos, getHeight() / 2);
  348. }
  349. totalLength = 0;
  350. for (int i = 0; i < tabs.size(); ++i)
  351. {
  352. TabBarButton* const tb = tabs.getUnchecked(i)->button;
  353. const int newLength = totalLength + tb->getBestTabLength (depth);
  354. if (i > 0 && newLength * minimumScale > tabsButtonPos)
  355. {
  356. totalLength += overlap;
  357. break;
  358. }
  359. numVisibleButtons = i + 1;
  360. totalLength = newLength - overlap;
  361. }
  362. scale = jmax (minimumScale, tabsButtonPos / (double) totalLength);
  363. }
  364. else
  365. {
  366. extraTabsButton = nullptr;
  367. }
  368. int pos = 0;
  369. TabBarButton* frontTab = nullptr;
  370. for (int i = 0; i < tabs.size(); ++i)
  371. {
  372. if (TabBarButton* const tb = getTabButton (i))
  373. {
  374. const int bestLength = roundToInt (scale * tb->getBestTabLength (depth));
  375. if (i < numVisibleButtons)
  376. {
  377. if (isVertical())
  378. tb->setBounds (0, pos, getWidth(), bestLength);
  379. else
  380. tb->setBounds (pos, 0, bestLength, getHeight());
  381. tb->toBack();
  382. if (i == currentTabIndex)
  383. frontTab = tb;
  384. tb->setVisible (true);
  385. }
  386. else
  387. {
  388. tb->setVisible (false);
  389. }
  390. pos += bestLength - overlap;
  391. }
  392. }
  393. behindFrontTab->setBounds (getLocalBounds());
  394. if (frontTab != nullptr)
  395. {
  396. frontTab->toFront (false);
  397. behindFrontTab->toBehind (frontTab);
  398. }
  399. }
  400. //==============================================================================
  401. Colour TabbedButtonBar::getTabBackgroundColour (const int tabIndex)
  402. {
  403. if (TabInfo* tab = tabs [tabIndex])
  404. return tab->colour;
  405. return Colours::transparentBlack;
  406. }
  407. void TabbedButtonBar::setTabBackgroundColour (const int tabIndex, Colour newColour)
  408. {
  409. if (TabInfo* const tab = tabs [tabIndex])
  410. {
  411. if (tab->colour != newColour)
  412. {
  413. tab->colour = newColour;
  414. repaint();
  415. }
  416. }
  417. }
  418. void TabbedButtonBar::extraItemsMenuCallback (int result, TabbedButtonBar* bar)
  419. {
  420. if (bar != nullptr && result > 0)
  421. bar->setCurrentTabIndex (result - 1);
  422. }
  423. void TabbedButtonBar::showExtraItemsMenu()
  424. {
  425. PopupMenu m;
  426. for (int i = 0; i < tabs.size(); ++i)
  427. {
  428. const TabInfo* const tab = tabs.getUnchecked(i);
  429. if (! tab->button->isVisible())
  430. m.addItem (i + 1, tab->name, true, i == currentTabIndex);
  431. }
  432. m.showMenuAsync (PopupMenu::Options().withTargetComponent (extraTabsButton),
  433. ModalCallbackFunction::forComponent (extraItemsMenuCallback, this));
  434. }
  435. //==============================================================================
  436. void TabbedButtonBar::currentTabChanged (const int, const String&)
  437. {
  438. }
  439. void TabbedButtonBar::popupMenuClickOnTab (const int, const String&)
  440. {
  441. }