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.

527 lines
15KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2013 - Raw Material Software Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. TabBarButton::TabBarButton (const String& name, TabbedButtonBar& owner_)
  18. : Button (name), owner (owner_), overlapPixels (0), extraCompPlacement (afterText)
  19. {
  20. setWantsKeyboardFocus (false);
  21. }
  22. TabBarButton::~TabBarButton() {}
  23. int TabBarButton::getIndex() const { return owner.indexOfTabButton (this); }
  24. Colour TabBarButton::getTabBackgroundColour() const { return owner.getTabBackgroundColour (getIndex()); }
  25. bool TabBarButton::isFrontTab() const { return getToggleState(); }
  26. void TabBarButton::paintButton (Graphics& g, const bool isMouseOverButton, const bool isButtonDown)
  27. {
  28. getLookAndFeel().drawTabButton (*this, g, isMouseOverButton, isButtonDown);
  29. }
  30. void TabBarButton::clicked (const ModifierKeys& mods)
  31. {
  32. if (mods.isPopupMenu())
  33. owner.popupMenuClickOnTab (getIndex(), getButtonText());
  34. else
  35. owner.setCurrentTabIndex (getIndex());
  36. }
  37. bool TabBarButton::hitTest (int mx, int my)
  38. {
  39. const Rectangle<int> area (getActiveArea());
  40. if (owner.isVertical())
  41. {
  42. if (isPositiveAndBelow (mx, getWidth())
  43. && my >= area.getY() + overlapPixels && my < area.getBottom() - overlapPixels)
  44. return true;
  45. }
  46. else
  47. {
  48. if (isPositiveAndBelow (my, getHeight())
  49. && mx >= area.getX() + overlapPixels && mx < area.getRight() - overlapPixels)
  50. return true;
  51. }
  52. Path p;
  53. getLookAndFeel().createTabButtonShape (*this, p, false, false);
  54. return p.contains ((float) (mx - area.getX()),
  55. (float) (my - area.getY()));
  56. }
  57. int TabBarButton::getBestTabLength (const int depth)
  58. {
  59. return getLookAndFeel().getTabButtonBestWidth (*this, depth);
  60. }
  61. void TabBarButton::calcAreas (Rectangle<int>& extraComp, Rectangle<int>& textArea) const
  62. {
  63. LookAndFeel& lf = getLookAndFeel();
  64. textArea = getActiveArea();
  65. const int depth = owner.isVertical() ? textArea.getWidth() : textArea.getHeight();
  66. const int overlap = lf.getTabButtonOverlap (depth);
  67. if (overlap > 0)
  68. {
  69. if (owner.isVertical())
  70. textArea.reduce (0, overlap);
  71. else
  72. textArea.reduce (overlap, 0);
  73. }
  74. if (extraComponent != nullptr)
  75. extraComp = lf.getTabButtonExtraComponentBounds (*this, textArea, *extraComponent);
  76. }
  77. Rectangle<int> TabBarButton::getTextArea() const
  78. {
  79. Rectangle<int> extraComp, textArea;
  80. calcAreas (extraComp, textArea);
  81. return textArea;
  82. }
  83. Rectangle<int> TabBarButton::getActiveArea() const
  84. {
  85. Rectangle<int> r (getLocalBounds());
  86. const int spaceAroundImage = getLookAndFeel().getTabButtonSpaceAroundImage();
  87. const TabbedButtonBar::Orientation orientation = owner.getOrientation();
  88. if (orientation != TabbedButtonBar::TabsAtLeft) r.removeFromRight (spaceAroundImage);
  89. if (orientation != TabbedButtonBar::TabsAtRight) r.removeFromLeft (spaceAroundImage);
  90. if (orientation != TabbedButtonBar::TabsAtBottom) r.removeFromTop (spaceAroundImage);
  91. if (orientation != TabbedButtonBar::TabsAtTop) r.removeFromBottom (spaceAroundImage);
  92. return r;
  93. }
  94. void TabBarButton::setExtraComponent (Component* comp, ExtraComponentPlacement placement)
  95. {
  96. jassert (extraCompPlacement == beforeText || extraCompPlacement == afterText);
  97. extraCompPlacement = placement;
  98. addAndMakeVisible (extraComponent = comp);
  99. resized();
  100. }
  101. void TabBarButton::childBoundsChanged (Component* c)
  102. {
  103. if (c == extraComponent)
  104. {
  105. owner.resized();
  106. resized();
  107. }
  108. }
  109. void TabBarButton::resized()
  110. {
  111. if (extraComponent != nullptr)
  112. {
  113. Rectangle<int> extraComp, textArea;
  114. calcAreas (extraComp, textArea);
  115. if (! extraComp.isEmpty())
  116. extraComponent->setBounds (extraComp);
  117. }
  118. }
  119. //==============================================================================
  120. class TabbedButtonBar::BehindFrontTabComp : public Component,
  121. public ButtonListener // (can't use Button::Listener due to idiotic VC2005 bug)
  122. {
  123. public:
  124. BehindFrontTabComp (TabbedButtonBar& owner_)
  125. : owner (owner_)
  126. {
  127. setInterceptsMouseClicks (false, false);
  128. }
  129. void paint (Graphics& g) override
  130. {
  131. getLookAndFeel().drawTabAreaBehindFrontButton (owner, g, getWidth(), getHeight());
  132. }
  133. void enablementChanged() override
  134. {
  135. repaint();
  136. }
  137. void buttonClicked (Button*) override
  138. {
  139. owner.showExtraItemsMenu();
  140. }
  141. private:
  142. TabbedButtonBar& owner;
  143. JUCE_DECLARE_NON_COPYABLE (BehindFrontTabComp)
  144. };
  145. //==============================================================================
  146. TabbedButtonBar::TabbedButtonBar (const Orientation orientation_)
  147. : orientation (orientation_),
  148. minimumScale (0.7),
  149. currentTabIndex (-1)
  150. {
  151. setInterceptsMouseClicks (false, true);
  152. addAndMakeVisible (behindFrontTab = new BehindFrontTabComp (*this));
  153. setFocusContainer (true);
  154. }
  155. TabbedButtonBar::~TabbedButtonBar()
  156. {
  157. tabs.clear();
  158. extraTabsButton = nullptr;
  159. }
  160. //==============================================================================
  161. void TabbedButtonBar::setOrientation (const Orientation newOrientation)
  162. {
  163. orientation = newOrientation;
  164. for (int i = getNumChildComponents(); --i >= 0;)
  165. getChildComponent (i)->resized();
  166. resized();
  167. }
  168. TabBarButton* TabbedButtonBar::createTabButton (const String& name, const int /*index*/)
  169. {
  170. return new TabBarButton (name, *this);
  171. }
  172. void TabbedButtonBar::setMinimumTabScaleFactor (double newMinimumScale)
  173. {
  174. minimumScale = newMinimumScale;
  175. resized();
  176. }
  177. //==============================================================================
  178. void TabbedButtonBar::clearTabs()
  179. {
  180. tabs.clear();
  181. extraTabsButton = nullptr;
  182. setCurrentTabIndex (-1);
  183. }
  184. void TabbedButtonBar::addTab (const String& tabName,
  185. Colour tabBackgroundColour,
  186. int insertIndex)
  187. {
  188. jassert (tabName.isNotEmpty()); // you have to give them all a name..
  189. if (tabName.isNotEmpty())
  190. {
  191. if (! isPositiveAndBelow (insertIndex, tabs.size()))
  192. insertIndex = tabs.size();
  193. TabInfo* const currentTab = tabs [currentTabIndex];
  194. TabInfo* newTab = new TabInfo();
  195. newTab->name = tabName;
  196. newTab->colour = tabBackgroundColour;
  197. newTab->button = createTabButton (tabName, insertIndex);
  198. jassert (newTab->button != nullptr);
  199. tabs.insert (insertIndex, newTab);
  200. currentTabIndex = tabs.indexOf (currentTab);
  201. addAndMakeVisible (newTab->button, insertIndex);
  202. resized();
  203. if (currentTabIndex < 0)
  204. setCurrentTabIndex (0);
  205. }
  206. }
  207. void TabbedButtonBar::setTabName (const int tabIndex, const String& newName)
  208. {
  209. if (TabInfo* const tab = tabs [tabIndex])
  210. {
  211. if (tab->name != newName)
  212. {
  213. tab->name = newName;
  214. tab->button->setButtonText (newName);
  215. resized();
  216. }
  217. }
  218. }
  219. void TabbedButtonBar::removeTab (const int tabIndex)
  220. {
  221. if (tabIndex == currentTabIndex)
  222. setCurrentTabIndex (-1);
  223. TabInfo* const currentTab = tabs [currentTabIndex];
  224. tabs.remove (tabIndex);
  225. currentTabIndex = tabs.indexOf (currentTab);
  226. resized();
  227. }
  228. void TabbedButtonBar::moveTab (const int currentIndex, const int newIndex)
  229. {
  230. TabInfo* const currentTab = tabs [currentTabIndex];
  231. tabs.move (currentIndex, newIndex);
  232. currentTabIndex = tabs.indexOf (currentTab);
  233. resized();
  234. }
  235. int TabbedButtonBar::getNumTabs() const
  236. {
  237. return tabs.size();
  238. }
  239. String TabbedButtonBar::getCurrentTabName() const
  240. {
  241. TabInfo* tab = tabs [currentTabIndex];
  242. return tab == nullptr ? String::empty : tab->name;
  243. }
  244. StringArray TabbedButtonBar::getTabNames() const
  245. {
  246. StringArray names;
  247. for (int i = 0; i < tabs.size(); ++i)
  248. names.add (tabs.getUnchecked(i)->name);
  249. return names;
  250. }
  251. void TabbedButtonBar::setCurrentTabIndex (int newIndex, const bool sendChangeMessage_)
  252. {
  253. if (currentTabIndex != newIndex)
  254. {
  255. if (! isPositiveAndBelow (newIndex, tabs.size()))
  256. newIndex = -1;
  257. currentTabIndex = newIndex;
  258. for (int i = 0; i < tabs.size(); ++i)
  259. {
  260. TabBarButton* tb = tabs.getUnchecked(i)->button;
  261. tb->setToggleState (i == newIndex, dontSendNotification);
  262. }
  263. resized();
  264. if (sendChangeMessage_)
  265. sendChangeMessage();
  266. currentTabChanged (newIndex, getCurrentTabName());
  267. }
  268. }
  269. TabBarButton* TabbedButtonBar::getTabButton (const int index) const
  270. {
  271. TabInfo* const tab = tabs[index];
  272. return tab == nullptr ? nullptr : static_cast <TabBarButton*> (tab->button);
  273. }
  274. int TabbedButtonBar::indexOfTabButton (const TabBarButton* button) const
  275. {
  276. for (int i = tabs.size(); --i >= 0;)
  277. if (tabs.getUnchecked(i)->button == button)
  278. return i;
  279. return -1;
  280. }
  281. void TabbedButtonBar::lookAndFeelChanged()
  282. {
  283. extraTabsButton = nullptr;
  284. resized();
  285. }
  286. void TabbedButtonBar::resized()
  287. {
  288. LookAndFeel& lf = getLookAndFeel();
  289. int depth = getWidth();
  290. int length = getHeight();
  291. if (! isVertical())
  292. std::swap (depth, length);
  293. const int overlap = lf.getTabButtonOverlap (depth) + lf.getTabButtonSpaceAroundImage() * 2;
  294. int totalLength = jmax (0, overlap);
  295. int numVisibleButtons = tabs.size();
  296. for (int i = 0; i < tabs.size(); ++i)
  297. {
  298. TabBarButton* const tb = tabs.getUnchecked(i)->button;
  299. totalLength += tb->getBestTabLength (depth) - overlap;
  300. tb->overlapPixels = jmax (0, overlap / 2);
  301. }
  302. double scale = 1.0;
  303. if (totalLength > length)
  304. scale = jmax (minimumScale, length / (double) totalLength);
  305. const bool isTooBig = (int) (totalLength * scale) > length;
  306. int tabsButtonPos = 0;
  307. if (isTooBig)
  308. {
  309. if (extraTabsButton == nullptr)
  310. {
  311. addAndMakeVisible (extraTabsButton = lf.createTabBarExtrasButton());
  312. extraTabsButton->addListener (behindFrontTab);
  313. extraTabsButton->setAlwaysOnTop (true);
  314. extraTabsButton->setTriggeredOnMouseDown (true);
  315. }
  316. const int buttonSize = jmin (proportionOfWidth (0.7f), proportionOfHeight (0.7f));
  317. extraTabsButton->setSize (buttonSize, buttonSize);
  318. if (isVertical())
  319. {
  320. tabsButtonPos = getHeight() - buttonSize / 2 - 1;
  321. extraTabsButton->setCentrePosition (getWidth() / 2, tabsButtonPos);
  322. }
  323. else
  324. {
  325. tabsButtonPos = getWidth() - buttonSize / 2 - 1;
  326. extraTabsButton->setCentrePosition (tabsButtonPos, getHeight() / 2);
  327. }
  328. totalLength = 0;
  329. for (int i = 0; i < tabs.size(); ++i)
  330. {
  331. TabBarButton* const tb = tabs.getUnchecked(i)->button;
  332. const int newLength = totalLength + tb->getBestTabLength (depth);
  333. if (i > 0 && newLength * minimumScale > tabsButtonPos)
  334. {
  335. totalLength += overlap;
  336. break;
  337. }
  338. numVisibleButtons = i + 1;
  339. totalLength = newLength - overlap;
  340. }
  341. scale = jmax (minimumScale, tabsButtonPos / (double) totalLength);
  342. }
  343. else
  344. {
  345. extraTabsButton = nullptr;
  346. }
  347. int pos = 0;
  348. TabBarButton* frontTab = nullptr;
  349. for (int i = 0; i < tabs.size(); ++i)
  350. {
  351. if (TabBarButton* const tb = getTabButton (i))
  352. {
  353. const int bestLength = roundToInt (scale * tb->getBestTabLength (depth));
  354. if (i < numVisibleButtons)
  355. {
  356. if (isVertical())
  357. tb->setBounds (0, pos, getWidth(), bestLength);
  358. else
  359. tb->setBounds (pos, 0, bestLength, getHeight());
  360. tb->toBack();
  361. if (i == currentTabIndex)
  362. frontTab = tb;
  363. tb->setVisible (true);
  364. }
  365. else
  366. {
  367. tb->setVisible (false);
  368. }
  369. pos += bestLength - overlap;
  370. }
  371. }
  372. behindFrontTab->setBounds (getLocalBounds());
  373. if (frontTab != nullptr)
  374. {
  375. frontTab->toFront (false);
  376. behindFrontTab->toBehind (frontTab);
  377. }
  378. }
  379. //==============================================================================
  380. Colour TabbedButtonBar::getTabBackgroundColour (const int tabIndex)
  381. {
  382. TabInfo* const tab = tabs [tabIndex];
  383. return tab == nullptr ? Colours::white : tab->colour;
  384. }
  385. void TabbedButtonBar::setTabBackgroundColour (const int tabIndex, Colour newColour)
  386. {
  387. if (TabInfo* const tab = tabs [tabIndex])
  388. {
  389. if (tab->colour != newColour)
  390. {
  391. tab->colour = newColour;
  392. repaint();
  393. }
  394. }
  395. }
  396. void TabbedButtonBar::extraItemsMenuCallback (int result, TabbedButtonBar* bar)
  397. {
  398. if (bar != nullptr && result > 0)
  399. bar->setCurrentTabIndex (result - 1);
  400. }
  401. void TabbedButtonBar::showExtraItemsMenu()
  402. {
  403. PopupMenu m;
  404. for (int i = 0; i < tabs.size(); ++i)
  405. {
  406. const TabInfo* const tab = tabs.getUnchecked(i);
  407. if (! tab->button->isVisible())
  408. m.addItem (i + 1, tab->name, true, i == currentTabIndex);
  409. }
  410. m.showMenuAsync (PopupMenu::Options().withTargetComponent (extraTabsButton),
  411. ModalCallbackFunction::forComponent (extraItemsMenuCallback, this));
  412. }
  413. //==============================================================================
  414. void TabbedButtonBar::currentTabChanged (const int, const String&)
  415. {
  416. }
  417. void TabbedButtonBar::popupMenuClickOnTab (const int, const String&)
  418. {
  419. }