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.

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