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.

584 lines
17KB

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