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.

juce_TabbedButtonBar.cpp 17KB

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