The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
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.

586 lines
16KB

  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& bar)
  22. : Button (name), owner (bar)
  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. auto 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. auto& lf = getLookAndFeel();
  68. textArea = getActiveArea();
  69. auto depth = owner.isVertical() ? textArea.getWidth() : textArea.getHeight();
  70. auto 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. auto 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. auto r = getLocalBounds();
  107. auto spaceAroundImage = getLookAndFeel().getTabButtonSpaceAroundImage();
  108. auto 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 (Orientation orientationToUse)
  167. : orientation (orientationToUse)
  168. {
  169. setInterceptsMouseClicks (false, true);
  170. addAndMakeVisible (behindFrontTab = new BehindFrontTabComp (*this));
  171. setFocusContainer (true);
  172. }
  173. TabbedButtonBar::~TabbedButtonBar()
  174. {
  175. tabs.clear();
  176. extraTabsButton.reset();
  177. }
  178. //==============================================================================
  179. void TabbedButtonBar::setOrientation (const Orientation newOrientation)
  180. {
  181. orientation = newOrientation;
  182. for (auto* child : getChildren())
  183. child->resized();
  184. resized();
  185. }
  186. TabBarButton* TabbedButtonBar::createTabButton (const String& name, const int /*index*/)
  187. {
  188. return new TabBarButton (name, *this);
  189. }
  190. void TabbedButtonBar::setMinimumTabScaleFactor (double newMinimumScale)
  191. {
  192. minimumScale = newMinimumScale;
  193. resized();
  194. }
  195. //==============================================================================
  196. void TabbedButtonBar::clearTabs()
  197. {
  198. tabs.clear();
  199. extraTabsButton.reset();
  200. setCurrentTabIndex (-1);
  201. }
  202. void TabbedButtonBar::addTab (const String& tabName,
  203. Colour tabBackgroundColour,
  204. int insertIndex)
  205. {
  206. jassert (tabName.isNotEmpty()); // you have to give them all a name..
  207. if (tabName.isNotEmpty())
  208. {
  209. if (! isPositiveAndBelow (insertIndex, tabs.size()))
  210. insertIndex = tabs.size();
  211. auto* currentTab = tabs[currentTabIndex];
  212. auto* newTab = new TabInfo();
  213. newTab->name = tabName;
  214. newTab->colour = tabBackgroundColour;
  215. newTab->button = createTabButton (tabName, insertIndex);
  216. jassert (newTab->button != nullptr);
  217. tabs.insert (insertIndex, newTab);
  218. currentTabIndex = tabs.indexOf (currentTab);
  219. addAndMakeVisible (newTab->button, insertIndex);
  220. resized();
  221. if (currentTabIndex < 0)
  222. setCurrentTabIndex (0);
  223. }
  224. }
  225. void TabbedButtonBar::setTabName (int tabIndex, const String& newName)
  226. {
  227. if (auto* tab = tabs[tabIndex])
  228. {
  229. if (tab->name != newName)
  230. {
  231. tab->name = newName;
  232. tab->button->setButtonText (newName);
  233. resized();
  234. }
  235. }
  236. }
  237. void TabbedButtonBar::removeTab (const int indexToRemove, const bool animate)
  238. {
  239. if (isPositiveAndBelow (indexToRemove, tabs.size()))
  240. {
  241. auto oldSelectedIndex = currentTabIndex;
  242. if (indexToRemove == currentTabIndex)
  243. oldSelectedIndex = -1;
  244. else if (indexToRemove < oldSelectedIndex)
  245. --oldSelectedIndex;
  246. tabs.remove (indexToRemove);
  247. setCurrentTabIndex (oldSelectedIndex);
  248. updateTabPositions (animate);
  249. }
  250. }
  251. void TabbedButtonBar::moveTab (const int currentIndex, const int newIndex, const bool animate)
  252. {
  253. auto* currentTab = tabs[currentTabIndex];
  254. tabs.move (currentIndex, newIndex);
  255. currentTabIndex = tabs.indexOf (currentTab);
  256. updateTabPositions (animate);
  257. }
  258. int TabbedButtonBar::getNumTabs() const
  259. {
  260. return tabs.size();
  261. }
  262. String TabbedButtonBar::getCurrentTabName() const
  263. {
  264. if (auto* tab = tabs [currentTabIndex])
  265. return tab->name;
  266. return {};
  267. }
  268. StringArray TabbedButtonBar::getTabNames() const
  269. {
  270. StringArray names;
  271. for (auto* t : tabs)
  272. names.add (t->name);
  273. return names;
  274. }
  275. void TabbedButtonBar::setCurrentTabIndex (int newIndex, bool shouldSendChangeMessage)
  276. {
  277. if (currentTabIndex != newIndex)
  278. {
  279. if (! isPositiveAndBelow (newIndex, tabs.size()))
  280. newIndex = -1;
  281. currentTabIndex = newIndex;
  282. for (int i = 0; i < tabs.size(); ++i)
  283. tabs.getUnchecked(i)->button->setToggleState (i == newIndex, dontSendNotification);
  284. resized();
  285. if (shouldSendChangeMessage)
  286. sendChangeMessage();
  287. currentTabChanged (newIndex, getCurrentTabName());
  288. }
  289. }
  290. TabBarButton* TabbedButtonBar::getTabButton (const int index) const
  291. {
  292. if (auto* tab = tabs[index])
  293. return static_cast<TabBarButton*> (tab->button);
  294. return nullptr;
  295. }
  296. int TabbedButtonBar::indexOfTabButton (const TabBarButton* button) const
  297. {
  298. for (int i = tabs.size(); --i >= 0;)
  299. if (tabs.getUnchecked(i)->button == button)
  300. return i;
  301. return -1;
  302. }
  303. Rectangle<int> TabbedButtonBar::getTargetBounds (TabBarButton* button) const
  304. {
  305. if (button == nullptr || indexOfTabButton (button) == -1)
  306. return {};
  307. auto& animator = Desktop::getInstance().getAnimator();
  308. return animator.isAnimating (button) ? animator.getComponentDestination (button)
  309. : button->getBounds();
  310. }
  311. void TabbedButtonBar::lookAndFeelChanged()
  312. {
  313. extraTabsButton.reset();
  314. resized();
  315. }
  316. void TabbedButtonBar::paint (Graphics& g)
  317. {
  318. getLookAndFeel().drawTabbedButtonBarBackground (*this, g);
  319. }
  320. void TabbedButtonBar::resized()
  321. {
  322. updateTabPositions (false);
  323. }
  324. //==============================================================================
  325. void TabbedButtonBar::updateTabPositions (bool animate)
  326. {
  327. auto& lf = getLookAndFeel();
  328. auto depth = getWidth();
  329. auto length = getHeight();
  330. if (! isVertical())
  331. std::swap (depth, length);
  332. auto overlap = lf.getTabButtonOverlap (depth) + lf.getTabButtonSpaceAroundImage() * 2;
  333. auto totalLength = jmax (0, overlap);
  334. auto numVisibleButtons = tabs.size();
  335. for (int i = 0; i < tabs.size(); ++i)
  336. {
  337. auto* tb = tabs.getUnchecked(i)->button.get();
  338. totalLength += tb->getBestTabLength (depth) - overlap;
  339. tb->overlapPixels = jmax (0, overlap / 2);
  340. }
  341. double scale = 1.0;
  342. if (totalLength > length)
  343. scale = jmax (minimumScale, length / (double) totalLength);
  344. const bool isTooBig = (int) (totalLength * scale) > length;
  345. int tabsButtonPos = 0;
  346. if (isTooBig)
  347. {
  348. if (extraTabsButton == nullptr)
  349. {
  350. addAndMakeVisible (extraTabsButton = lf.createTabBarExtrasButton());
  351. extraTabsButton->addListener (behindFrontTab);
  352. extraTabsButton->setAlwaysOnTop (true);
  353. extraTabsButton->setTriggeredOnMouseDown (true);
  354. }
  355. auto buttonSize = jmin (proportionOfWidth (0.7f), proportionOfHeight (0.7f));
  356. extraTabsButton->setSize (buttonSize, buttonSize);
  357. if (isVertical())
  358. {
  359. tabsButtonPos = getHeight() - buttonSize / 2 - 1;
  360. extraTabsButton->setCentrePosition (getWidth() / 2, tabsButtonPos);
  361. }
  362. else
  363. {
  364. tabsButtonPos = getWidth() - buttonSize / 2 - 1;
  365. extraTabsButton->setCentrePosition (tabsButtonPos, getHeight() / 2);
  366. }
  367. totalLength = 0;
  368. for (int i = 0; i < tabs.size(); ++i)
  369. {
  370. auto* tb = tabs.getUnchecked(i)->button.get();
  371. auto newLength = totalLength + tb->getBestTabLength (depth);
  372. if (i > 0 && newLength * minimumScale > tabsButtonPos)
  373. {
  374. totalLength += overlap;
  375. break;
  376. }
  377. numVisibleButtons = i + 1;
  378. totalLength = newLength - overlap;
  379. }
  380. scale = jmax (minimumScale, tabsButtonPos / (double) totalLength);
  381. }
  382. else
  383. {
  384. extraTabsButton.reset();
  385. }
  386. int pos = 0;
  387. TabBarButton* frontTab = nullptr;
  388. auto& animator = Desktop::getInstance().getAnimator();
  389. for (int i = 0; i < tabs.size(); ++i)
  390. {
  391. if (auto* tb = getTabButton (i))
  392. {
  393. auto bestLength = roundToInt (scale * tb->getBestTabLength (depth));
  394. if (i < numVisibleButtons)
  395. {
  396. auto newBounds = isVertical() ? Rectangle<int> (0, pos, getWidth(), bestLength)
  397. : Rectangle<int> (pos, 0, bestLength, getHeight());
  398. if (animate)
  399. {
  400. animator.animateComponent (tb, newBounds, 1.0f, 200, false, 3.0, 0.0);
  401. }
  402. else
  403. {
  404. animator.cancelAnimation (tb, false);
  405. tb->setBounds (newBounds);
  406. }
  407. tb->toBack();
  408. if (i == currentTabIndex)
  409. frontTab = tb;
  410. tb->setVisible (true);
  411. }
  412. else
  413. {
  414. tb->setVisible (false);
  415. }
  416. pos += bestLength - overlap;
  417. }
  418. }
  419. behindFrontTab->setBounds (getLocalBounds());
  420. if (frontTab != nullptr)
  421. {
  422. frontTab->toFront (false);
  423. behindFrontTab->toBehind (frontTab);
  424. }
  425. }
  426. //==============================================================================
  427. Colour TabbedButtonBar::getTabBackgroundColour (int tabIndex)
  428. {
  429. if (auto* tab = tabs[tabIndex])
  430. return tab->colour;
  431. return Colours::transparentBlack;
  432. }
  433. void TabbedButtonBar::setTabBackgroundColour (int tabIndex, Colour newColour)
  434. {
  435. if (auto* tab = tabs [tabIndex])
  436. {
  437. if (tab->colour != newColour)
  438. {
  439. tab->colour = newColour;
  440. repaint();
  441. }
  442. }
  443. }
  444. void TabbedButtonBar::extraItemsMenuCallback (int result, TabbedButtonBar* bar)
  445. {
  446. if (bar != nullptr && result > 0)
  447. bar->setCurrentTabIndex (result - 1);
  448. }
  449. void TabbedButtonBar::showExtraItemsMenu()
  450. {
  451. PopupMenu m;
  452. for (int i = 0; i < tabs.size(); ++i)
  453. {
  454. auto* tab = tabs.getUnchecked(i);
  455. if (! tab->button->isVisible())
  456. m.addItem (i + 1, tab->name, true, i == currentTabIndex);
  457. }
  458. m.showMenuAsync (PopupMenu::Options().withTargetComponent (extraTabsButton),
  459. ModalCallbackFunction::forComponent (extraItemsMenuCallback, this));
  460. }
  461. //==============================================================================
  462. void TabbedButtonBar::currentTabChanged (int, const String&) {}
  463. void TabbedButtonBar::popupMenuClickOnTab (int, const String&) {}
  464. } // namespace juce