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.

1734 lines
59KB

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