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.

1847 lines
62KB

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