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.

571 lines
16KB

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