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.

1732 lines
58KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library - "Jules' Utility Class Extensions"
  4. Copyright 2004-11 by Raw Material Software Ltd.
  5. ------------------------------------------------------------------------------
  6. JUCE can be redistributed and/or modified under the terms of the GNU General
  7. Public License (Version 2), as published by the Free Software Foundation.
  8. A copy of the license is included in the JUCE distribution, or can be found
  9. online at www.gnu.org/licenses.
  10. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  11. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  13. ------------------------------------------------------------------------------
  14. To release a closed-source product which uses JUCE, commercial licenses are
  15. available: visit www.rawmaterialsoftware.com/juce for more information.
  16. ==============================================================================
  17. */
  18. BEGIN_JUCE_NAMESPACE
  19. //==============================================================================
  20. class PopupMenu::Item
  21. {
  22. public:
  23. //==============================================================================
  24. Item()
  25. : itemId (0), active (true), isSeparator (true), isTicked (false),
  26. usesColour (false), commandManager (nullptr)
  27. {
  28. }
  29. Item (const int itemId_,
  30. const String& text_,
  31. const bool active_,
  32. const bool isTicked_,
  33. const Image& im,
  34. const Colour& textColour_,
  35. const bool usesColour_,
  36. CustomComponent* const customComp_,
  37. const PopupMenu* const subMenu_,
  38. ApplicationCommandManager* const commandManager_)
  39. : itemId (itemId_), text (text_), textColour (textColour_),
  40. active (active_), isSeparator (false), isTicked (isTicked_),
  41. usesColour (usesColour_), image (im), customComp (customComp_),
  42. commandManager (commandManager_)
  43. {
  44. if (subMenu_ != nullptr)
  45. subMenu = new PopupMenu (*subMenu_);
  46. if (commandManager_ != nullptr && itemId_ != 0)
  47. {
  48. String shortcutKey;
  49. Array <KeyPress> keyPresses (commandManager_->getKeyMappings()
  50. ->getKeyPressesAssignedToCommand (itemId_));
  51. for (int i = 0; i < keyPresses.size(); ++i)
  52. {
  53. const String key (keyPresses.getReference(i).getTextDescriptionWithIcons());
  54. if (shortcutKey.isNotEmpty())
  55. shortcutKey << ", ";
  56. if (key.length() == 1 && key[0] < 128)
  57. shortcutKey << "shortcut: '" << key << '\'';
  58. else
  59. shortcutKey << key;
  60. }
  61. shortcutKey = shortcutKey.trim();
  62. if (shortcutKey.isNotEmpty())
  63. text << "<end>" << shortcutKey;
  64. }
  65. }
  66. Item (const Item& other)
  67. : itemId (other.itemId),
  68. text (other.text),
  69. textColour (other.textColour),
  70. active (other.active),
  71. isSeparator (other.isSeparator),
  72. isTicked (other.isTicked),
  73. usesColour (other.usesColour),
  74. image (other.image),
  75. customComp (other.customComp),
  76. commandManager (other.commandManager)
  77. {
  78. if (other.subMenu != nullptr)
  79. subMenu = new PopupMenu (*(other.subMenu));
  80. }
  81. bool canBeTriggered() const noexcept { return active && ! (isSeparator || (subMenu != nullptr)); }
  82. bool hasActiveSubMenu() const noexcept { return active && (subMenu != nullptr); }
  83. //==============================================================================
  84. const int itemId;
  85. String text;
  86. const Colour textColour;
  87. const bool active, isSeparator, isTicked, usesColour;
  88. Image image;
  89. ReferenceCountedObjectPtr <CustomComponent> customComp;
  90. ScopedPointer <PopupMenu> subMenu;
  91. ApplicationCommandManager* const commandManager;
  92. private:
  93. Item& operator= (const Item&);
  94. JUCE_LEAK_DETECTOR (Item);
  95. };
  96. //==============================================================================
  97. class PopupMenu::ItemComponent : public Component
  98. {
  99. public:
  100. //==============================================================================
  101. ItemComponent (const PopupMenu::Item& itemInfo_, int standardItemHeight, Component* const parent)
  102. : itemInfo (itemInfo_),
  103. isHighlighted (false)
  104. {
  105. if (itemInfo.customComp != nullptr)
  106. addAndMakeVisible (itemInfo.customComp);
  107. parent->addAndMakeVisible (this);
  108. int itemW = 80;
  109. int itemH = 16;
  110. getIdealSize (itemW, itemH, standardItemHeight);
  111. setSize (itemW, jlimit (2, 600, itemH));
  112. addMouseListener (parent, false);
  113. }
  114. ~ItemComponent()
  115. {
  116. if (itemInfo.customComp != nullptr)
  117. removeChildComponent (itemInfo.customComp);
  118. }
  119. void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight)
  120. {
  121. if (itemInfo.customComp != nullptr)
  122. itemInfo.customComp->getIdealSize (idealWidth, idealHeight);
  123. else
  124. getLookAndFeel().getIdealPopupMenuItemSize (itemInfo.text,
  125. itemInfo.isSeparator,
  126. standardItemHeight,
  127. idealWidth, idealHeight);
  128. }
  129. void paint (Graphics& g)
  130. {
  131. if (itemInfo.customComp == nullptr)
  132. {
  133. String mainText (itemInfo.text);
  134. String endText;
  135. const int endIndex = mainText.indexOf ("<end>");
  136. if (endIndex >= 0)
  137. {
  138. endText = mainText.substring (endIndex + 5).trim();
  139. mainText = mainText.substring (0, endIndex);
  140. }
  141. getLookAndFeel()
  142. .drawPopupMenuItem (g, getWidth(), getHeight(),
  143. itemInfo.isSeparator,
  144. itemInfo.active,
  145. isHighlighted,
  146. itemInfo.isTicked,
  147. itemInfo.subMenu != 0,
  148. mainText, endText,
  149. itemInfo.image.isValid() ? &itemInfo.image : 0,
  150. itemInfo.usesColour ? &(itemInfo.textColour) : 0);
  151. }
  152. }
  153. void resized()
  154. {
  155. if (getNumChildComponents() > 0)
  156. getChildComponent(0)->setBounds (2, 0, getWidth() - 4, getHeight());
  157. }
  158. void setHighlighted (bool shouldBeHighlighted)
  159. {
  160. shouldBeHighlighted = shouldBeHighlighted && itemInfo.active;
  161. if (isHighlighted != shouldBeHighlighted)
  162. {
  163. isHighlighted = shouldBeHighlighted;
  164. if (itemInfo.customComp != nullptr)
  165. itemInfo.customComp->setHighlighted (shouldBeHighlighted);
  166. repaint();
  167. }
  168. }
  169. PopupMenu::Item itemInfo;
  170. private:
  171. bool isHighlighted;
  172. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent);
  173. };
  174. //==============================================================================
  175. namespace PopupMenuSettings
  176. {
  177. const int scrollZone = 24;
  178. const int borderSize = 2;
  179. const int timerInterval = 50;
  180. const int dismissCommandId = 0x6287345f;
  181. static bool menuWasHiddenBecauseOfAppChange = false;
  182. }
  183. //==============================================================================
  184. class PopupMenu::Window : public Component,
  185. private Timer
  186. {
  187. public:
  188. //==============================================================================
  189. Window (const PopupMenu& menu, Window* const owner_, const Rectangle<int>& target,
  190. const bool alignToRectangle, const int itemIdThatMustBeVisible,
  191. const int minimumWidth_, const int maximumNumColumns_,
  192. const int standardItemHeight_, const bool dismissOnMouseUp_,
  193. ApplicationCommandManager** const managerOfChosenCommand_,
  194. Component* const componentAttachedTo_)
  195. : Component ("menu"),
  196. owner (owner_),
  197. activeSubMenu (nullptr),
  198. managerOfChosenCommand (managerOfChosenCommand_),
  199. componentAttachedTo (componentAttachedTo_),
  200. componentAttachedToOriginal (componentAttachedTo_),
  201. minimumWidth (minimumWidth_),
  202. maximumNumColumns (maximumNumColumns_),
  203. standardItemHeight (standardItemHeight_),
  204. isOver (false),
  205. hasBeenOver (false),
  206. isDown (false),
  207. needsToScroll (false),
  208. dismissOnMouseUp (dismissOnMouseUp_),
  209. hideOnExit (false),
  210. disableMouseMoves (false),
  211. hasAnyJuceCompHadFocus (false),
  212. numColumns (0),
  213. contentHeight (0),
  214. childYOffset (0),
  215. menuCreationTime (Time::getMillisecondCounter()),
  216. lastMouseMoveTime (0),
  217. timeEnteredCurrentChildComp (0),
  218. scrollAcceleration (1.0)
  219. {
  220. lastFocused = lastScroll = menuCreationTime;
  221. setWantsKeyboardFocus (false);
  222. setMouseClickGrabsKeyboardFocus (false);
  223. setAlwaysOnTop (true);
  224. setLookAndFeel (menu.lookAndFeel);
  225. setOpaque (getLookAndFeel().findColour (PopupMenu::backgroundColourId).isOpaque() || ! Desktop::canUseSemiTransparentWindows());
  226. for (int i = 0; i < menu.items.size(); ++i)
  227. {
  228. PopupMenu::Item* const item = menu.items.getUnchecked(i);
  229. if (i < menu.items.size() - 1 || ! item->isSeparator)
  230. items.add (new PopupMenu::ItemComponent (*item, standardItemHeight, this));
  231. }
  232. calculateWindowPos (target, alignToRectangle);
  233. setTopLeftPosition (windowPos.getPosition());
  234. updateYPositions();
  235. if (itemIdThatMustBeVisible != 0)
  236. {
  237. const int y = target.getY() - windowPos.getY();
  238. ensureItemIsVisible (itemIdThatMustBeVisible,
  239. isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1);
  240. }
  241. resizeToBestWindowPos();
  242. addToDesktop (ComponentPeer::windowIsTemporary
  243. | ComponentPeer::windowIgnoresKeyPresses
  244. | getLookAndFeel().getMenuWindowFlags());
  245. getActiveWindows().add (this);
  246. Desktop::getInstance().addGlobalMouseListener (this);
  247. }
  248. ~Window()
  249. {
  250. getActiveWindows().removeValue (this);
  251. Desktop::getInstance().removeGlobalMouseListener (this);
  252. activeSubMenu = nullptr;
  253. items.clear();
  254. }
  255. //==============================================================================
  256. static Window* create (const PopupMenu& menu,
  257. bool dismissOnMouseUp,
  258. Window* const owner_,
  259. const Rectangle<int>& target,
  260. int minimumWidth,
  261. int maximumNumColumns,
  262. int standardItemHeight,
  263. bool alignToRectangle,
  264. int itemIdThatMustBeVisible,
  265. ApplicationCommandManager** managerOfChosenCommand,
  266. Component* componentAttachedTo)
  267. {
  268. if (menu.items.size() > 0)
  269. return new Window (menu, owner_, target, alignToRectangle, itemIdThatMustBeVisible,
  270. minimumWidth, maximumNumColumns, standardItemHeight, dismissOnMouseUp,
  271. managerOfChosenCommand, componentAttachedTo);
  272. return nullptr;
  273. }
  274. //==============================================================================
  275. void paint (Graphics& g)
  276. {
  277. if (isOpaque())
  278. g.fillAll (Colours::white);
  279. getLookAndFeel().drawPopupMenuBackground (g, getWidth(), getHeight());
  280. }
  281. void paintOverChildren (Graphics& g)
  282. {
  283. if (isScrolling())
  284. {
  285. LookAndFeel& lf = getLookAndFeel();
  286. if (isScrollZoneActive (false))
  287. lf.drawPopupMenuUpDownArrow (g, getWidth(), PopupMenuSettings::scrollZone, true);
  288. if (isScrollZoneActive (true))
  289. {
  290. g.setOrigin (0, getHeight() - PopupMenuSettings::scrollZone);
  291. lf.drawPopupMenuUpDownArrow (g, getWidth(), PopupMenuSettings::scrollZone, false);
  292. }
  293. }
  294. }
  295. bool isScrollZoneActive (bool bottomOne) const
  296. {
  297. return isScrolling()
  298. && (bottomOne ? childYOffset < contentHeight - windowPos.getHeight()
  299. : childYOffset > 0);
  300. }
  301. //==============================================================================
  302. // hide this and all sub-comps
  303. void hide (const PopupMenu::Item* const item, const bool makeInvisible)
  304. {
  305. if (isVisible())
  306. {
  307. WeakReference<Component> deletionChecker (this);
  308. activeSubMenu = nullptr;
  309. currentChild = nullptr;
  310. if (item != nullptr
  311. && item->commandManager != nullptr
  312. && item->itemId != 0)
  313. {
  314. *managerOfChosenCommand = item->commandManager;
  315. }
  316. exitModalState (item != nullptr ? item->itemId : 0);
  317. if (makeInvisible && (deletionChecker != nullptr))
  318. setVisible (false);
  319. }
  320. }
  321. void dismissMenu (const PopupMenu::Item* const item)
  322. {
  323. if (owner != nullptr)
  324. {
  325. owner->dismissMenu (item);
  326. }
  327. else
  328. {
  329. if (item != nullptr)
  330. {
  331. // need a copy of this on the stack as the one passed in will get deleted during this call
  332. const PopupMenu::Item mi (*item);
  333. hide (&mi, false);
  334. }
  335. else
  336. {
  337. hide (0, false);
  338. }
  339. }
  340. }
  341. //==============================================================================
  342. void mouseMove (const MouseEvent&) { timerCallback(); }
  343. void mouseDown (const MouseEvent&) { timerCallback(); }
  344. void mouseDrag (const MouseEvent&) { timerCallback(); }
  345. void mouseUp (const MouseEvent&) { timerCallback(); }
  346. void mouseWheelMove (const MouseEvent&, float /*amountX*/, float amountY)
  347. {
  348. alterChildYPos (roundToInt (-10.0f * amountY * PopupMenuSettings::scrollZone));
  349. lastMouse = Point<int> (-1, -1);
  350. }
  351. bool keyPressed (const KeyPress& key)
  352. {
  353. if (key.isKeyCode (KeyPress::downKey))
  354. {
  355. selectNextItem (1);
  356. }
  357. else if (key.isKeyCode (KeyPress::upKey))
  358. {
  359. selectNextItem (-1);
  360. }
  361. else if (key.isKeyCode (KeyPress::leftKey))
  362. {
  363. if (owner != nullptr)
  364. {
  365. Component::SafePointer<Window> parentWindow (owner);
  366. PopupMenu::ItemComponent* currentChildOfParent = parentWindow->currentChild;
  367. hide (0, true);
  368. if (parentWindow != nullptr)
  369. parentWindow->setCurrentlyHighlightedChild (currentChildOfParent);
  370. disableTimerUntilMouseMoves();
  371. }
  372. else if (componentAttachedTo != nullptr)
  373. {
  374. componentAttachedTo->keyPressed (key);
  375. }
  376. }
  377. else if (key.isKeyCode (KeyPress::rightKey))
  378. {
  379. disableTimerUntilMouseMoves();
  380. if (showSubMenuFor (currentChild))
  381. {
  382. if (activeSubMenu != nullptr && activeSubMenu->isVisible())
  383. activeSubMenu->selectNextItem (1);
  384. }
  385. else if (componentAttachedTo != nullptr)
  386. {
  387. componentAttachedTo->keyPressed (key);
  388. }
  389. }
  390. else if (key.isKeyCode (KeyPress::returnKey))
  391. {
  392. triggerCurrentlyHighlightedItem();
  393. }
  394. else if (key.isKeyCode (KeyPress::escapeKey))
  395. {
  396. dismissMenu (0);
  397. }
  398. else
  399. {
  400. return false;
  401. }
  402. return true;
  403. }
  404. void inputAttemptWhenModal()
  405. {
  406. WeakReference<Component> deletionChecker (this);
  407. timerCallback();
  408. if (deletionChecker != nullptr && ! isOverAnyMenu())
  409. {
  410. if (componentAttachedTo != nullptr)
  411. {
  412. // we want to dismiss the menu, but if we do it synchronously, then
  413. // the mouse-click will be allowed to pass through. That's good, except
  414. // when the user clicks on the button that orginally popped the menu up,
  415. // as they'll expect the menu to go away, and in fact it'll just
  416. // come back. So only dismiss synchronously if they're not on the original
  417. // comp that we're attached to.
  418. const Point<int> mousePos (componentAttachedTo->getMouseXYRelative());
  419. if (componentAttachedTo->reallyContains (mousePos, true))
  420. {
  421. postCommandMessage (PopupMenuSettings::dismissCommandId); // dismiss asynchrounously
  422. return;
  423. }
  424. }
  425. dismissMenu (0);
  426. }
  427. }
  428. void handleCommandMessage (int commandId)
  429. {
  430. Component::handleCommandMessage (commandId);
  431. if (commandId == PopupMenuSettings::dismissCommandId)
  432. dismissMenu (0);
  433. }
  434. //==============================================================================
  435. void timerCallback()
  436. {
  437. if (! isVisible())
  438. return;
  439. if (componentAttachedTo != componentAttachedToOriginal)
  440. {
  441. dismissMenu (0);
  442. return;
  443. }
  444. Window* currentlyModalWindow = dynamic_cast <Window*> (Component::getCurrentlyModalComponent());
  445. if (currentlyModalWindow != nullptr && ! treeContains (currentlyModalWindow))
  446. return;
  447. startTimer (PopupMenuSettings::timerInterval); // do this in case it was called from a mouse
  448. // move rather than a real timer callback
  449. const Point<int> globalMousePos (Desktop::getMousePosition());
  450. const Point<int> localMousePos (getLocalPoint (nullptr, globalMousePos));
  451. const uint32 now = Time::getMillisecondCounter();
  452. if (now > timeEnteredCurrentChildComp + 100
  453. && reallyContains (localMousePos, true)
  454. && currentChild != nullptr
  455. && (! disableMouseMoves)
  456. && ! (activeSubMenu != nullptr && activeSubMenu->isVisible()))
  457. {
  458. showSubMenuFor (currentChild);
  459. }
  460. if (globalMousePos != lastMouse || now > lastMouseMoveTime + 350)
  461. {
  462. highlightItemUnderMouse (globalMousePos, localMousePos);
  463. }
  464. bool overScrollArea = false;
  465. if (isScrolling()
  466. && (isOver || (isDown && isPositiveAndBelow (localMousePos.getX(), getWidth())))
  467. && ((isScrollZoneActive (false) && localMousePos.getY() < PopupMenuSettings::scrollZone)
  468. || (isScrollZoneActive (true) && localMousePos.getY() > getHeight() - PopupMenuSettings::scrollZone)))
  469. {
  470. if (now > lastScroll + 20)
  471. {
  472. scrollAcceleration = jmin (4.0, scrollAcceleration * 1.04);
  473. int amount = 0;
  474. for (int i = 0; i < items.size() && amount == 0; ++i)
  475. amount = ((int) scrollAcceleration) * items.getUnchecked(i)->getHeight();
  476. alterChildYPos (localMousePos.getY() < PopupMenuSettings::scrollZone ? -amount : amount);
  477. lastScroll = now;
  478. }
  479. overScrollArea = true;
  480. lastMouse = Point<int> (-1, -1); // trigger a mouse-move
  481. }
  482. else
  483. {
  484. scrollAcceleration = 1.0;
  485. }
  486. const bool wasDown = isDown;
  487. bool isOverAny = isOverAnyMenu();
  488. if (hideOnExit && hasBeenOver && (! isOverAny) && activeSubMenu != nullptr)
  489. {
  490. activeSubMenu->updateMouseOverStatus (globalMousePos);
  491. isOverAny = isOverAnyMenu();
  492. }
  493. if (hideOnExit && hasBeenOver && ! isOverAny)
  494. {
  495. hide (0, true);
  496. }
  497. else
  498. {
  499. isDown = hasBeenOver
  500. && (ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown()
  501. || ModifierKeys::getCurrentModifiersRealtime().isAnyMouseButtonDown());
  502. bool anyFocused = Process::isForegroundProcess();
  503. if (anyFocused && Component::getCurrentlyFocusedComponent() == nullptr)
  504. {
  505. // because no component at all may have focus, our test here will
  506. // only be triggered when something has focus and then loses it.
  507. anyFocused = ! hasAnyJuceCompHadFocus;
  508. for (int i = ComponentPeer::getNumPeers(); --i >= 0;)
  509. {
  510. if (ComponentPeer::getPeer (i)->isFocused())
  511. {
  512. anyFocused = true;
  513. hasAnyJuceCompHadFocus = true;
  514. break;
  515. }
  516. }
  517. }
  518. if (! anyFocused)
  519. {
  520. if (now > lastFocused + 10)
  521. {
  522. PopupMenuSettings::menuWasHiddenBecauseOfAppChange = true;
  523. dismissMenu (0);
  524. return; // may have been deleted by the previous call..
  525. }
  526. }
  527. else if (wasDown && now > menuCreationTime + 250
  528. && ! (isDown || overScrollArea))
  529. {
  530. isOver = reallyContains (localMousePos, true);
  531. if (isOver)
  532. {
  533. triggerCurrentlyHighlightedItem();
  534. }
  535. else if ((hasBeenOver || ! dismissOnMouseUp) && ! isOverAny)
  536. {
  537. dismissMenu (0);
  538. }
  539. return; // may have been deleted by the previous calls..
  540. }
  541. else
  542. {
  543. lastFocused = now;
  544. }
  545. }
  546. }
  547. static Array<Window*>& getActiveWindows()
  548. {
  549. static Array<Window*> activeMenuWindows;
  550. return activeMenuWindows;
  551. }
  552. //==============================================================================
  553. private:
  554. Window* owner;
  555. OwnedArray <PopupMenu::ItemComponent> items;
  556. Component::SafePointer<PopupMenu::ItemComponent> currentChild;
  557. ScopedPointer <Window> activeSubMenu;
  558. ApplicationCommandManager** managerOfChosenCommand;
  559. WeakReference<Component> componentAttachedTo;
  560. Component* componentAttachedToOriginal;
  561. Rectangle<int> windowPos;
  562. Point<int> lastMouse;
  563. int minimumWidth, maximumNumColumns, standardItemHeight;
  564. bool isOver, hasBeenOver, isDown, needsToScroll;
  565. bool dismissOnMouseUp, hideOnExit, disableMouseMoves, hasAnyJuceCompHadFocus;
  566. int numColumns, contentHeight, childYOffset;
  567. Array<int> columnWidths;
  568. uint32 menuCreationTime, lastFocused, lastScroll, lastMouseMoveTime, timeEnteredCurrentChildComp;
  569. double scrollAcceleration;
  570. //==============================================================================
  571. bool overlaps (const Rectangle<int>& r) const
  572. {
  573. return r.intersects (getBounds())
  574. || (owner != nullptr && owner->overlaps (r));
  575. }
  576. bool isOverAnyMenu() const
  577. {
  578. return owner != nullptr ? owner->isOverAnyMenu()
  579. : isOverChildren();
  580. }
  581. bool isOverChildren() const
  582. {
  583. return isVisible()
  584. && (isOver || (activeSubMenu != nullptr && activeSubMenu->isOverChildren()));
  585. }
  586. void updateMouseOverStatus (const Point<int>& globalMousePos)
  587. {
  588. const Point<int> relPos (getLocalPoint (nullptr, globalMousePos));
  589. isOver = reallyContains (relPos, true);
  590. if (activeSubMenu != nullptr)
  591. activeSubMenu->updateMouseOverStatus (globalMousePos);
  592. }
  593. bool treeContains (const Window* const window) const noexcept
  594. {
  595. const Window* mw = this;
  596. while (mw->owner != nullptr)
  597. mw = mw->owner;
  598. while (mw != nullptr)
  599. {
  600. if (mw == window)
  601. return true;
  602. mw = mw->activeSubMenu;
  603. }
  604. return false;
  605. }
  606. //==============================================================================
  607. void calculateWindowPos (const Rectangle<int>& target, const bool alignToRectangle)
  608. {
  609. const Rectangle<int> mon (Desktop::getInstance()
  610. .getMonitorAreaContaining (target.getCentre(),
  611. #if JUCE_MAC
  612. true));
  613. #else
  614. false)); // on windows, don't stop the menu overlapping the taskbar
  615. #endif
  616. const int maxMenuHeight = mon.getHeight() - 24;
  617. int x, y, widthToUse, heightToUse;
  618. layoutMenuItems (mon.getWidth() - 24, maxMenuHeight, widthToUse, heightToUse);
  619. if (alignToRectangle)
  620. {
  621. x = target.getX();
  622. const int spaceUnder = mon.getHeight() - (target.getBottom() - mon.getY());
  623. const int spaceOver = target.getY() - mon.getY();
  624. if (heightToUse < spaceUnder - 30 || spaceUnder >= spaceOver)
  625. y = target.getBottom();
  626. else
  627. y = target.getY() - heightToUse;
  628. }
  629. else
  630. {
  631. bool tendTowardsRight = target.getCentreX() < mon.getCentreX();
  632. if (owner != nullptr)
  633. {
  634. if (owner->owner != nullptr)
  635. {
  636. const bool ownerGoingRight = (owner->getX() + owner->getWidth() / 2
  637. > owner->owner->getX() + owner->owner->getWidth() / 2);
  638. if (ownerGoingRight && target.getRight() + widthToUse < mon.getRight() - 4)
  639. tendTowardsRight = true;
  640. else if ((! ownerGoingRight) && target.getX() > widthToUse + 4)
  641. tendTowardsRight = false;
  642. }
  643. else if (target.getRight() + widthToUse < mon.getRight() - 32)
  644. {
  645. tendTowardsRight = true;
  646. }
  647. }
  648. const int biggestSpace = jmax (mon.getRight() - target.getRight(),
  649. target.getX() - mon.getX()) - 32;
  650. if (biggestSpace < widthToUse)
  651. {
  652. layoutMenuItems (biggestSpace + target.getWidth() / 3, maxMenuHeight, widthToUse, heightToUse);
  653. if (numColumns > 1)
  654. layoutMenuItems (biggestSpace - 4, maxMenuHeight, widthToUse, heightToUse);
  655. tendTowardsRight = (mon.getRight() - target.getRight()) >= (target.getX() - mon.getX());
  656. }
  657. if (tendTowardsRight)
  658. x = jmin (mon.getRight() - widthToUse - 4, target.getRight());
  659. else
  660. x = jmax (mon.getX() + 4, target.getX() - widthToUse);
  661. y = target.getY();
  662. if (target.getCentreY() > mon.getCentreY())
  663. y = jmax (mon.getY(), target.getBottom() - heightToUse);
  664. }
  665. x = jmax (mon.getX() + 1, jmin (mon.getRight() - (widthToUse + 6), x));
  666. y = jmax (mon.getY() + 1, jmin (mon.getBottom() - (heightToUse + 6), y));
  667. windowPos.setBounds (x, y, widthToUse, heightToUse);
  668. // sets this flag if it's big enough to obscure any of its parent menus
  669. hideOnExit = owner != nullptr
  670. && owner->windowPos.intersects (windowPos.expanded (-4, -4));
  671. }
  672. void layoutMenuItems (const int maxMenuW, const int maxMenuH, int& width, int& height)
  673. {
  674. numColumns = 0;
  675. contentHeight = 0;
  676. int totalW;
  677. do
  678. {
  679. ++numColumns;
  680. totalW = workOutBestSize (maxMenuW);
  681. if (totalW > maxMenuW)
  682. {
  683. numColumns = jmax (1, numColumns - 1);
  684. totalW = workOutBestSize (maxMenuW); // to update col widths
  685. break;
  686. }
  687. else if (totalW > maxMenuW / 2 || contentHeight < maxMenuH)
  688. {
  689. break;
  690. }
  691. } while (numColumns < maximumNumColumns);
  692. const int actualH = jmin (contentHeight, maxMenuH);
  693. needsToScroll = contentHeight > actualH;
  694. width = updateYPositions();
  695. height = actualH + PopupMenuSettings::borderSize * 2;
  696. }
  697. int workOutBestSize (const int maxMenuW)
  698. {
  699. int totalW = 0;
  700. contentHeight = 0;
  701. int childNum = 0;
  702. for (int col = 0; col < numColumns; ++col)
  703. {
  704. int i, colW = standardItemHeight, colH = 0;
  705. const int numChildren = jmin (items.size() - childNum,
  706. (items.size() + numColumns - 1) / numColumns);
  707. for (i = numChildren; --i >= 0;)
  708. {
  709. colW = jmax (colW, items.getUnchecked (childNum + i)->getWidth());
  710. colH += items.getUnchecked (childNum + i)->getHeight();
  711. }
  712. colW = jmin (maxMenuW / jmax (1, numColumns - 2), colW + PopupMenuSettings::borderSize * 2);
  713. columnWidths.set (col, colW);
  714. totalW += colW;
  715. contentHeight = jmax (contentHeight, colH);
  716. childNum += numChildren;
  717. }
  718. if (totalW < minimumWidth)
  719. {
  720. totalW = minimumWidth;
  721. for (int col = 0; col < numColumns; ++col)
  722. columnWidths.set (0, totalW / numColumns);
  723. }
  724. return totalW;
  725. }
  726. void ensureItemIsVisible (const int itemId, int wantedY)
  727. {
  728. jassert (itemId != 0)
  729. for (int i = items.size(); --i >= 0;)
  730. {
  731. PopupMenu::ItemComponent* const m = items.getUnchecked(i);
  732. if (m != nullptr
  733. && m->itemInfo.itemId == itemId
  734. && windowPos.getHeight() > PopupMenuSettings::scrollZone * 4)
  735. {
  736. const int currentY = m->getY();
  737. if (wantedY > 0 || currentY < 0 || m->getBottom() > windowPos.getHeight())
  738. {
  739. if (wantedY < 0)
  740. wantedY = jlimit (PopupMenuSettings::scrollZone,
  741. jmax (PopupMenuSettings::scrollZone,
  742. windowPos.getHeight() - (PopupMenuSettings::scrollZone + m->getHeight())),
  743. currentY);
  744. const Rectangle<int> mon (Desktop::getInstance().getMonitorAreaContaining (windowPos.getPosition(), true));
  745. int deltaY = wantedY - currentY;
  746. windowPos.setSize (jmin (windowPos.getWidth(), mon.getWidth()),
  747. jmin (windowPos.getHeight(), mon.getHeight()));
  748. const int newY = jlimit (mon.getY(),
  749. mon.getBottom() - windowPos.getHeight(),
  750. windowPos.getY() + deltaY);
  751. deltaY -= newY - windowPos.getY();
  752. childYOffset -= deltaY;
  753. windowPos.setPosition (windowPos.getX(), newY);
  754. updateYPositions();
  755. }
  756. break;
  757. }
  758. }
  759. }
  760. void resizeToBestWindowPos()
  761. {
  762. Rectangle<int> r (windowPos);
  763. if (childYOffset < 0)
  764. {
  765. r.setBounds (r.getX(), r.getY() - childYOffset,
  766. r.getWidth(), r.getHeight() + childYOffset);
  767. }
  768. else if (childYOffset > 0)
  769. {
  770. const int spaceAtBottom = r.getHeight() - (contentHeight - childYOffset);
  771. if (spaceAtBottom > 0)
  772. r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom);
  773. }
  774. setBounds (r);
  775. updateYPositions();
  776. }
  777. void alterChildYPos (const int delta)
  778. {
  779. if (isScrolling())
  780. {
  781. childYOffset += delta;
  782. if (delta < 0)
  783. {
  784. childYOffset = jmax (childYOffset, 0);
  785. }
  786. else if (delta > 0)
  787. {
  788. childYOffset = jmin (childYOffset,
  789. contentHeight - windowPos.getHeight() + PopupMenuSettings::borderSize);
  790. }
  791. updateYPositions();
  792. }
  793. else
  794. {
  795. childYOffset = 0;
  796. }
  797. resizeToBestWindowPos();
  798. repaint();
  799. }
  800. int updateYPositions()
  801. {
  802. int x = 0;
  803. int childNum = 0;
  804. for (int col = 0; col < numColumns; ++col)
  805. {
  806. const int numChildren = jmin (items.size() - childNum,
  807. (items.size() + numColumns - 1) / numColumns);
  808. const int colW = columnWidths [col];
  809. int y = PopupMenuSettings::borderSize - (childYOffset + (getY() - windowPos.getY()));
  810. for (int i = 0; i < numChildren; ++i)
  811. {
  812. Component* const c = items.getUnchecked (childNum + i);
  813. c->setBounds (x, y, colW, c->getHeight());
  814. y += c->getHeight();
  815. }
  816. x += colW;
  817. childNum += numChildren;
  818. }
  819. return x;
  820. }
  821. bool isScrolling() const noexcept
  822. {
  823. return childYOffset != 0 || needsToScroll;
  824. }
  825. void setCurrentlyHighlightedChild (PopupMenu::ItemComponent* const child)
  826. {
  827. if (currentChild != nullptr)
  828. currentChild->setHighlighted (false);
  829. currentChild = child;
  830. if (currentChild != nullptr)
  831. {
  832. currentChild->setHighlighted (true);
  833. timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter();
  834. }
  835. }
  836. bool showSubMenuFor (PopupMenu::ItemComponent* const childComp)
  837. {
  838. activeSubMenu = nullptr;
  839. if (childComp != nullptr && childComp->itemInfo.hasActiveSubMenu())
  840. {
  841. activeSubMenu = Window::create (*(childComp->itemInfo.subMenu),
  842. dismissOnMouseUp,
  843. this,
  844. childComp->getScreenBounds(),
  845. 0, maximumNumColumns,
  846. standardItemHeight,
  847. false, 0, managerOfChosenCommand,
  848. componentAttachedTo);
  849. if (activeSubMenu != nullptr)
  850. {
  851. activeSubMenu->setVisible (true);
  852. activeSubMenu->enterModalState (false);
  853. activeSubMenu->toFront (false);
  854. return true;
  855. }
  856. }
  857. return false;
  858. }
  859. void highlightItemUnderMouse (const Point<int>& globalMousePos, const Point<int>& localMousePos)
  860. {
  861. isOver = reallyContains (localMousePos, true);
  862. if (isOver)
  863. hasBeenOver = true;
  864. if (lastMouse.getDistanceFrom (globalMousePos) > 2)
  865. {
  866. lastMouseMoveTime = Time::getApproximateMillisecondCounter();
  867. if (disableMouseMoves && isOver)
  868. disableMouseMoves = false;
  869. }
  870. if (disableMouseMoves || (activeSubMenu != nullptr && activeSubMenu->isOverChildren()))
  871. return;
  872. bool isMovingTowardsMenu = false;
  873. if (isOver && (activeSubMenu != nullptr) && globalMousePos != lastMouse)
  874. {
  875. // try to intelligently guess whether the user is moving the mouse towards a currently-open
  876. // submenu. To do this, look at whether the mouse stays inside a triangular region that
  877. // extends from the last mouse pos to the submenu's rectangle..
  878. float subX = (float) activeSubMenu->getScreenX();
  879. if (activeSubMenu->getX() > getX())
  880. {
  881. lastMouse -= Point<int> (2, 0); // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels
  882. }
  883. else
  884. {
  885. lastMouse += Point<int> (2, 0);
  886. subX += activeSubMenu->getWidth();
  887. }
  888. Path areaTowardsSubMenu;
  889. areaTowardsSubMenu.addTriangle ((float) lastMouse.getX(), (float) lastMouse.getY(),
  890. subX, (float) activeSubMenu->getScreenY(),
  891. subX, (float) (activeSubMenu->getScreenY() + activeSubMenu->getHeight()));
  892. isMovingTowardsMenu = areaTowardsSubMenu.contains ((float) globalMousePos.getX(), (float) globalMousePos.getY());
  893. }
  894. lastMouse = globalMousePos;
  895. if (! isMovingTowardsMenu)
  896. {
  897. Component* c = getComponentAt (localMousePos.getX(), localMousePos.getY());
  898. if (c == this)
  899. c = nullptr;
  900. PopupMenu::ItemComponent* mic = dynamic_cast <PopupMenu::ItemComponent*> (c);
  901. if (mic == nullptr && c != nullptr)
  902. mic = c->findParentComponentOfClass ((PopupMenu::ItemComponent*) nullptr);
  903. if (mic != currentChild
  904. && (isOver || (activeSubMenu == nullptr) || ! activeSubMenu->isVisible()))
  905. {
  906. if (isOver && (c != nullptr) && (activeSubMenu != nullptr))
  907. activeSubMenu->hide (0, true);
  908. if (! isOver)
  909. mic = nullptr;
  910. setCurrentlyHighlightedChild (mic);
  911. }
  912. }
  913. }
  914. void triggerCurrentlyHighlightedItem()
  915. {
  916. if (currentChild != nullptr
  917. && currentChild->itemInfo.canBeTriggered()
  918. && (currentChild->itemInfo.customComp == nullptr
  919. || currentChild->itemInfo.customComp->isTriggeredAutomatically()))
  920. {
  921. dismissMenu (&currentChild->itemInfo);
  922. }
  923. }
  924. void selectNextItem (const int delta)
  925. {
  926. disableTimerUntilMouseMoves();
  927. PopupMenu::ItemComponent* mic = nullptr;
  928. bool wasLastOne = (currentChild == nullptr);
  929. const int numItems = items.size();
  930. for (int i = 0; i < numItems + 1; ++i)
  931. {
  932. int index = (delta > 0) ? i : (numItems - 1 - i);
  933. index = (index + numItems) % numItems;
  934. mic = items.getUnchecked (index);
  935. if (mic != nullptr && (mic->itemInfo.canBeTriggered() || mic->itemInfo.hasActiveSubMenu())
  936. && wasLastOne)
  937. break;
  938. if (mic == currentChild)
  939. wasLastOne = true;
  940. }
  941. setCurrentlyHighlightedChild (mic);
  942. }
  943. void disableTimerUntilMouseMoves()
  944. {
  945. disableMouseMoves = true;
  946. if (owner != nullptr)
  947. owner->disableTimerUntilMouseMoves();
  948. }
  949. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Window);
  950. };
  951. //==============================================================================
  952. PopupMenu::PopupMenu()
  953. : lookAndFeel (nullptr)
  954. {
  955. }
  956. PopupMenu::PopupMenu (const PopupMenu& other)
  957. : lookAndFeel (other.lookAndFeel)
  958. {
  959. items.addCopiesOf (other.items);
  960. }
  961. PopupMenu& PopupMenu::operator= (const PopupMenu& other)
  962. {
  963. if (this != &other)
  964. {
  965. lookAndFeel = other.lookAndFeel;
  966. clear();
  967. items.addCopiesOf (other.items);
  968. }
  969. return *this;
  970. }
  971. #if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
  972. PopupMenu::PopupMenu (PopupMenu&& other) noexcept
  973. : lookAndFeel (other.lookAndFeel)
  974. {
  975. items.swapWithArray (other.items);
  976. }
  977. PopupMenu& PopupMenu::operator= (PopupMenu&& other) noexcept
  978. {
  979. if (this != &other)
  980. {
  981. items.swapWithArray (other.items);
  982. lookAndFeel = other.lookAndFeel;
  983. }
  984. return *this;
  985. }
  986. #endif
  987. PopupMenu::~PopupMenu()
  988. {
  989. }
  990. void PopupMenu::clear()
  991. {
  992. items.clear();
  993. }
  994. void PopupMenu::addItem (const int itemResultId, const String& itemText,
  995. const bool isActive, const bool isTicked, const Image& iconToUse)
  996. {
  997. jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user
  998. // didn't pick anything, so you shouldn't use it as the id
  999. // for an item..
  1000. items.add (new Item (itemResultId, itemText, isActive, isTicked, iconToUse,
  1001. Colours::black, false, 0, 0, 0));
  1002. }
  1003. void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager,
  1004. const int commandID,
  1005. const String& displayName)
  1006. {
  1007. jassert (commandManager != nullptr && commandID != 0);
  1008. const ApplicationCommandInfo* const registeredInfo = commandManager->getCommandForID (commandID);
  1009. if (registeredInfo != nullptr)
  1010. {
  1011. ApplicationCommandInfo info (*registeredInfo);
  1012. ApplicationCommandTarget* const target = commandManager->getTargetForCommand (commandID, info);
  1013. items.add (new Item (commandID,
  1014. displayName.isNotEmpty() ? displayName
  1015. : info.shortName,
  1016. target != nullptr && (info.flags & ApplicationCommandInfo::isDisabled) == 0,
  1017. (info.flags & ApplicationCommandInfo::isTicked) != 0,
  1018. Image::null,
  1019. Colours::black,
  1020. false,
  1021. 0, 0,
  1022. commandManager));
  1023. }
  1024. }
  1025. void PopupMenu::addColouredItem (const int itemResultId,
  1026. const String& itemText,
  1027. const Colour& itemTextColour,
  1028. const bool isActive,
  1029. const bool isTicked,
  1030. const Image& iconToUse)
  1031. {
  1032. jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user
  1033. // didn't pick anything, so you shouldn't use it as the id
  1034. // for an item..
  1035. items.add (new Item (itemResultId, itemText, isActive, isTicked, iconToUse,
  1036. itemTextColour, true, 0, 0, 0));
  1037. }
  1038. //==============================================================================
  1039. void PopupMenu::addCustomItem (const int itemResultId, CustomComponent* const customComponent)
  1040. {
  1041. jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user
  1042. // didn't pick anything, so you shouldn't use it as the id
  1043. // for an item..
  1044. items.add (new Item (itemResultId, String::empty, true, false, Image::null,
  1045. Colours::black, false, customComponent, 0, 0));
  1046. }
  1047. class NormalComponentWrapper : public PopupMenu::CustomComponent
  1048. {
  1049. public:
  1050. NormalComponentWrapper (Component* const comp, const int w, const int h,
  1051. const bool triggerMenuItemAutomaticallyWhenClicked)
  1052. : PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked),
  1053. width (w), height (h)
  1054. {
  1055. addAndMakeVisible (comp);
  1056. }
  1057. void getIdealSize (int& idealWidth, int& idealHeight)
  1058. {
  1059. idealWidth = width;
  1060. idealHeight = height;
  1061. }
  1062. void resized()
  1063. {
  1064. if (getChildComponent(0) != nullptr)
  1065. getChildComponent(0)->setBounds (getLocalBounds());
  1066. }
  1067. private:
  1068. const int width, height;
  1069. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper);
  1070. };
  1071. void PopupMenu::addCustomItem (const int itemResultId,
  1072. Component* customComponent,
  1073. int idealWidth, int idealHeight,
  1074. const bool triggerMenuItemAutomaticallyWhenClicked)
  1075. {
  1076. addCustomItem (itemResultId,
  1077. new NormalComponentWrapper (customComponent, idealWidth, idealHeight,
  1078. triggerMenuItemAutomaticallyWhenClicked));
  1079. }
  1080. //==============================================================================
  1081. void PopupMenu::addSubMenu (const String& subMenuName,
  1082. const PopupMenu& subMenu,
  1083. const bool isActive,
  1084. const Image& iconToUse,
  1085. const bool isTicked)
  1086. {
  1087. items.add (new Item (0, subMenuName, isActive && (subMenu.getNumItems() > 0), isTicked,
  1088. iconToUse, Colours::black, false, 0, &subMenu, 0));
  1089. }
  1090. void PopupMenu::addSeparator()
  1091. {
  1092. if (items.size() > 0 && ! items.getLast()->isSeparator)
  1093. items.add (new Item());
  1094. }
  1095. //==============================================================================
  1096. class HeaderItemComponent : public PopupMenu::CustomComponent
  1097. {
  1098. public:
  1099. HeaderItemComponent (const String& name)
  1100. : PopupMenu::CustomComponent (false)
  1101. {
  1102. setName (name);
  1103. }
  1104. void paint (Graphics& g)
  1105. {
  1106. Font f (getLookAndFeel().getPopupMenuFont());
  1107. f.setBold (true);
  1108. g.setFont (f);
  1109. g.setColour (findColour (PopupMenu::headerTextColourId));
  1110. g.drawFittedText (getName(),
  1111. 12, 0, getWidth() - 16, proportionOfHeight (0.8f),
  1112. Justification::bottomLeft, 1);
  1113. }
  1114. void getIdealSize (int& idealWidth, int& idealHeight)
  1115. {
  1116. getLookAndFeel().getIdealPopupMenuItemSize (getName(), false, -1, idealWidth, idealHeight);
  1117. idealHeight += idealHeight / 2;
  1118. idealWidth += idealWidth / 4;
  1119. }
  1120. private:
  1121. JUCE_LEAK_DETECTOR (HeaderItemComponent);
  1122. };
  1123. void PopupMenu::addSectionHeader (const String& title)
  1124. {
  1125. addCustomItem (0X4734a34f, new HeaderItemComponent (title));
  1126. }
  1127. //==============================================================================
  1128. PopupMenu::Options::Options()
  1129. : targetComponent (nullptr),
  1130. visibleItemID (0),
  1131. minWidth (0),
  1132. maxColumns (0),
  1133. standardHeight (0)
  1134. {
  1135. targetArea.setPosition (Desktop::getMousePosition());
  1136. }
  1137. const PopupMenu::Options PopupMenu::Options::withTargetComponent (Component* comp) const
  1138. {
  1139. Options o (*this);
  1140. o.targetComponent = comp;
  1141. if (comp != nullptr)
  1142. o.targetArea = comp->getScreenBounds();
  1143. return o;
  1144. }
  1145. const PopupMenu::Options PopupMenu::Options::withTargetScreenArea (const Rectangle<int>& area) const
  1146. {
  1147. Options o (*this);
  1148. o.targetArea = area;
  1149. return o;
  1150. }
  1151. const PopupMenu::Options PopupMenu::Options::withMinimumWidth (int w) const
  1152. {
  1153. Options o (*this);
  1154. o.minWidth = w;
  1155. return o;
  1156. }
  1157. const PopupMenu::Options PopupMenu::Options::withMaximumNumColumns (int cols) const
  1158. {
  1159. Options o (*this);
  1160. o.maxColumns = cols;
  1161. return o;
  1162. }
  1163. const PopupMenu::Options PopupMenu::Options::withStandardItemHeight (int height) const
  1164. {
  1165. Options o (*this);
  1166. o.standardHeight = height;
  1167. return o;
  1168. }
  1169. const PopupMenu::Options PopupMenu::Options::withItemThatMustBeVisible (int idOfItemToBeVisible) const
  1170. {
  1171. Options o (*this);
  1172. o.visibleItemID = idOfItemToBeVisible;
  1173. return o;
  1174. }
  1175. Component* PopupMenu::createWindow (const Options& options,
  1176. ApplicationCommandManager** managerOfChosenCommand) const
  1177. {
  1178. return Window::create (*this, ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown(),
  1179. 0, options.targetArea, options.minWidth, options.maxColumns > 0 ? options.maxColumns : 7,
  1180. options.standardHeight, ! options.targetArea.isEmpty(), options.visibleItemID,
  1181. managerOfChosenCommand, options.targetComponent);
  1182. }
  1183. //==============================================================================
  1184. // This invokes any command manager commands and deletes the menu window when it is dismissed
  1185. class PopupMenuCompletionCallback : public ModalComponentManager::Callback
  1186. {
  1187. public:
  1188. PopupMenuCompletionCallback()
  1189. : managerOfChosenCommand (nullptr),
  1190. prevFocused (Component::getCurrentlyFocusedComponent()),
  1191. prevTopLevel (prevFocused != nullptr ? prevFocused->getTopLevelComponent() : 0)
  1192. {
  1193. PopupMenuSettings::menuWasHiddenBecauseOfAppChange = false;
  1194. }
  1195. void modalStateFinished (int result)
  1196. {
  1197. if (managerOfChosenCommand != nullptr && result != 0)
  1198. {
  1199. ApplicationCommandTarget::InvocationInfo info (result);
  1200. info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
  1201. managerOfChosenCommand->invoke (info, true);
  1202. }
  1203. // (this would be the place to fade out the component, if that's what's required)
  1204. component = nullptr;
  1205. if (! PopupMenuSettings::menuWasHiddenBecauseOfAppChange)
  1206. {
  1207. if (prevTopLevel != nullptr)
  1208. prevTopLevel->toFront (true);
  1209. if (prevFocused != nullptr)
  1210. prevFocused->grabKeyboardFocus();
  1211. }
  1212. }
  1213. ApplicationCommandManager* managerOfChosenCommand;
  1214. ScopedPointer<Component> component;
  1215. WeakReference<Component> prevFocused, prevTopLevel;
  1216. private:
  1217. JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback);
  1218. };
  1219. int PopupMenu::showWithOptionalCallback (const Options& options, ModalComponentManager::Callback* const userCallback,
  1220. const bool canBeModal)
  1221. {
  1222. ScopedPointer<ModalComponentManager::Callback> userCallbackDeleter (userCallback);
  1223. ScopedPointer<PopupMenuCompletionCallback> callback (new PopupMenuCompletionCallback());
  1224. Component* window = createWindow (options, &(callback->managerOfChosenCommand));
  1225. if (window == nullptr)
  1226. return 0;
  1227. callback->component = window;
  1228. window->enterModalState (false, userCallbackDeleter.release());
  1229. ModalComponentManager::getInstance()->attachCallback (window, callback.release());
  1230. window->toFront (false); // need to do this after making it modal, or it could
  1231. // be stuck behind other comps that are already modal..
  1232. #if JUCE_MODAL_LOOPS_PERMITTED
  1233. return (userCallback == nullptr && canBeModal) ? window->runModalLoop() : 0;
  1234. #else
  1235. jassert (userCallback != nullptr && canBeModal);
  1236. return 0;
  1237. #endif
  1238. }
  1239. //==============================================================================
  1240. #if JUCE_MODAL_LOOPS_PERMITTED
  1241. int PopupMenu::showMenu (const Options& options)
  1242. {
  1243. return showWithOptionalCallback (options, 0, true);
  1244. }
  1245. #endif
  1246. void PopupMenu::showMenuAsync (const Options& options, ModalComponentManager::Callback* userCallback)
  1247. {
  1248. #if ! JUCE_MODAL_LOOPS_PERMITTED
  1249. jassert (userCallback != nullptr);
  1250. #endif
  1251. showWithOptionalCallback (options, userCallback, false);
  1252. }
  1253. //==============================================================================
  1254. #if JUCE_MODAL_LOOPS_PERMITTED
  1255. int PopupMenu::show (const int itemIdThatMustBeVisible,
  1256. const int minimumWidth, const int maximumNumColumns,
  1257. const int standardItemHeight,
  1258. ModalComponentManager::Callback* callback)
  1259. {
  1260. return showWithOptionalCallback (Options().withItemThatMustBeVisible (itemIdThatMustBeVisible)
  1261. .withMinimumWidth (minimumWidth)
  1262. .withMaximumNumColumns (maximumNumColumns)
  1263. .withStandardItemHeight (standardItemHeight),
  1264. callback, true);
  1265. }
  1266. int PopupMenu::showAt (const Rectangle<int>& screenAreaToAttachTo,
  1267. const int itemIdThatMustBeVisible,
  1268. const int minimumWidth, const int maximumNumColumns,
  1269. const int standardItemHeight,
  1270. ModalComponentManager::Callback* callback)
  1271. {
  1272. return showWithOptionalCallback (Options().withTargetScreenArea (screenAreaToAttachTo)
  1273. .withItemThatMustBeVisible (itemIdThatMustBeVisible)
  1274. .withMinimumWidth (minimumWidth)
  1275. .withMaximumNumColumns (maximumNumColumns)
  1276. .withStandardItemHeight (standardItemHeight),
  1277. callback, true);
  1278. }
  1279. int PopupMenu::showAt (Component* componentToAttachTo,
  1280. const int itemIdThatMustBeVisible,
  1281. const int minimumWidth, const int maximumNumColumns,
  1282. const int standardItemHeight,
  1283. ModalComponentManager::Callback* callback)
  1284. {
  1285. Options options (Options().withItemThatMustBeVisible (itemIdThatMustBeVisible)
  1286. .withMinimumWidth (minimumWidth)
  1287. .withMaximumNumColumns (maximumNumColumns)
  1288. .withStandardItemHeight (standardItemHeight));
  1289. if (componentToAttachTo != nullptr)
  1290. options = options.withTargetComponent (componentToAttachTo);
  1291. return showWithOptionalCallback (options, callback, true);
  1292. }
  1293. #endif
  1294. bool JUCE_CALLTYPE PopupMenu::dismissAllActiveMenus()
  1295. {
  1296. const int numWindows = Window::getActiveWindows().size();
  1297. for (int i = numWindows; --i >= 0;)
  1298. {
  1299. Window* const pmw = Window::getActiveWindows()[i];
  1300. if (pmw != nullptr)
  1301. pmw->dismissMenu (0);
  1302. }
  1303. return numWindows > 0;
  1304. }
  1305. //==============================================================================
  1306. int PopupMenu::getNumItems() const noexcept
  1307. {
  1308. int num = 0;
  1309. for (int i = items.size(); --i >= 0;)
  1310. if (! items.getUnchecked(i)->isSeparator)
  1311. ++num;
  1312. return num;
  1313. }
  1314. bool PopupMenu::containsCommandItem (const int commandID) const
  1315. {
  1316. for (int i = items.size(); --i >= 0;)
  1317. {
  1318. const Item* const mi = items.getUnchecked (i);
  1319. if ((mi->itemId == commandID && mi->commandManager != nullptr)
  1320. || (mi->subMenu != nullptr && mi->subMenu->containsCommandItem (commandID)))
  1321. {
  1322. return true;
  1323. }
  1324. }
  1325. return false;
  1326. }
  1327. bool PopupMenu::containsAnyActiveItems() const noexcept
  1328. {
  1329. for (int i = items.size(); --i >= 0;)
  1330. {
  1331. const Item* const mi = items.getUnchecked (i);
  1332. if (mi->subMenu != nullptr)
  1333. {
  1334. if (mi->subMenu->containsAnyActiveItems())
  1335. return true;
  1336. }
  1337. else if (mi->active)
  1338. {
  1339. return true;
  1340. }
  1341. }
  1342. return false;
  1343. }
  1344. void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel)
  1345. {
  1346. lookAndFeel = newLookAndFeel;
  1347. }
  1348. //==============================================================================
  1349. PopupMenu::CustomComponent::CustomComponent (const bool isTriggeredAutomatically_)
  1350. : isHighlighted (false),
  1351. triggeredAutomatically (isTriggeredAutomatically_)
  1352. {
  1353. }
  1354. PopupMenu::CustomComponent::~CustomComponent()
  1355. {
  1356. }
  1357. void PopupMenu::CustomComponent::setHighlighted (bool shouldBeHighlighted)
  1358. {
  1359. isHighlighted = shouldBeHighlighted;
  1360. repaint();
  1361. }
  1362. void PopupMenu::CustomComponent::triggerMenuItem()
  1363. {
  1364. PopupMenu::ItemComponent* const mic = dynamic_cast <PopupMenu::ItemComponent*> (getParentComponent());
  1365. if (mic != nullptr)
  1366. {
  1367. PopupMenu::Window* const pmw = dynamic_cast <PopupMenu::Window*> (mic->getParentComponent());
  1368. if (pmw != nullptr)
  1369. {
  1370. pmw->dismissMenu (&mic->itemInfo);
  1371. }
  1372. else
  1373. {
  1374. // something must have gone wrong with the component hierarchy if this happens..
  1375. jassertfalse;
  1376. }
  1377. }
  1378. else
  1379. {
  1380. // why isn't this component inside a menu? Not much point triggering the item if
  1381. // there's no menu.
  1382. jassertfalse;
  1383. }
  1384. }
  1385. //==============================================================================
  1386. PopupMenu::MenuItemIterator::MenuItemIterator (const PopupMenu& menu_)
  1387. : subMenu (nullptr),
  1388. itemId (0),
  1389. isSeparator (false),
  1390. isTicked (false),
  1391. isEnabled (false),
  1392. isCustomComponent (false),
  1393. isSectionHeader (false),
  1394. customColour (0),
  1395. menu (menu_),
  1396. index (0)
  1397. {
  1398. }
  1399. PopupMenu::MenuItemIterator::~MenuItemIterator()
  1400. {
  1401. }
  1402. bool PopupMenu::MenuItemIterator::next()
  1403. {
  1404. if (index >= menu.items.size())
  1405. return false;
  1406. const Item* const item = menu.items.getUnchecked (index);
  1407. ++index;
  1408. itemName = item->customComp != nullptr ? item->customComp->getName() : item->text;
  1409. subMenu = item->subMenu;
  1410. itemId = item->itemId;
  1411. isSeparator = item->isSeparator;
  1412. isTicked = item->isTicked;
  1413. isEnabled = item->active;
  1414. isSectionHeader = dynamic_cast <HeaderItemComponent*> (static_cast <CustomComponent*> (item->customComp)) != nullptr;
  1415. isCustomComponent = (! isSectionHeader) && item->customComp != nullptr;
  1416. customColour = item->usesColour ? &(item->textColour) : 0;
  1417. customImage = item->image;
  1418. commandManager = item->commandManager;
  1419. return true;
  1420. }
  1421. END_JUCE_NAMESPACE