Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1771 lines
61KB

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