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.

2365 lines
82KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 7 technical preview.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For the technical preview this file cannot be licensed commercially.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. namespace juce
  14. {
  15. namespace PopupMenuSettings
  16. {
  17. const int scrollZone = 24;
  18. const int dismissCommandId = 0x6287345f;
  19. static bool menuWasHiddenBecauseOfAppChange = false;
  20. }
  21. //==============================================================================
  22. struct PopupMenu::HelperClasses
  23. {
  24. class MouseSourceState;
  25. struct MenuWindow;
  26. static bool canBeTriggered (const PopupMenu::Item& item) noexcept
  27. {
  28. return item.isEnabled
  29. && item.itemID != 0
  30. && ! item.isSectionHeader
  31. && (item.customComponent == nullptr || item.customComponent->isTriggeredAutomatically());
  32. }
  33. static bool hasActiveSubMenu (const PopupMenu::Item& item) noexcept
  34. {
  35. return item.isEnabled
  36. && item.subMenu != nullptr
  37. && item.subMenu->items.size() > 0;
  38. }
  39. //==============================================================================
  40. struct HeaderItemComponent : public PopupMenu::CustomComponent
  41. {
  42. HeaderItemComponent (const String& name, const Options& opts)
  43. : CustomComponent (false), options (opts)
  44. {
  45. setName (name);
  46. }
  47. void paint (Graphics& g) override
  48. {
  49. getLookAndFeel().drawPopupMenuSectionHeaderWithOptions (g,
  50. getLocalBounds(),
  51. getName(),
  52. options);
  53. }
  54. void getIdealSize (int& idealWidth, int& idealHeight) override
  55. {
  56. getLookAndFeel().getIdealPopupMenuItemSizeWithOptions (getName(),
  57. false,
  58. -1,
  59. idealWidth,
  60. idealHeight,
  61. options);
  62. idealHeight += idealHeight / 2;
  63. idealWidth += idealWidth / 4;
  64. }
  65. std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
  66. {
  67. return nullptr;
  68. }
  69. const Options& options;
  70. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HeaderItemComponent)
  71. };
  72. //==============================================================================
  73. struct ItemComponent : public Component
  74. {
  75. ItemComponent (const PopupMenu::Item& i, const PopupMenu::Options& o, MenuWindow& parent)
  76. : item (i), parentWindow (parent), options (o), customComp (i.customComponent)
  77. {
  78. if (item.isSectionHeader)
  79. customComp = *new HeaderItemComponent (item.text, options);
  80. if (customComp != nullptr)
  81. {
  82. setItem (*customComp, &item);
  83. addAndMakeVisible (*customComp);
  84. }
  85. parent.addAndMakeVisible (this);
  86. updateShortcutKeyDescription();
  87. int itemW = 80;
  88. int itemH = 16;
  89. getIdealSize (itemW, itemH, options.getStandardItemHeight());
  90. setSize (itemW, jlimit (1, 600, itemH));
  91. addMouseListener (&parent, false);
  92. }
  93. ~ItemComponent() override
  94. {
  95. if (customComp != nullptr)
  96. setItem (*customComp, nullptr);
  97. removeChildComponent (customComp.get());
  98. }
  99. void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight)
  100. {
  101. if (customComp != nullptr)
  102. customComp->getIdealSize (idealWidth, idealHeight);
  103. else
  104. getLookAndFeel().getIdealPopupMenuItemSizeWithOptions (getTextForMeasurement(),
  105. item.isSeparator,
  106. standardItemHeight,
  107. idealWidth, idealHeight,
  108. options);
  109. }
  110. void paint (Graphics& g) override
  111. {
  112. if (customComp == nullptr)
  113. getLookAndFeel().drawPopupMenuItemWithOptions (g, getLocalBounds(),
  114. isHighlighted,
  115. item,
  116. options);
  117. }
  118. void resized() override
  119. {
  120. if (auto* child = getChildComponent (0))
  121. {
  122. const auto border = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options);
  123. child->setBounds (getLocalBounds().reduced (border, 0));
  124. }
  125. }
  126. void setHighlighted (bool shouldBeHighlighted)
  127. {
  128. shouldBeHighlighted = shouldBeHighlighted && item.isEnabled;
  129. if (isHighlighted != shouldBeHighlighted)
  130. {
  131. isHighlighted = shouldBeHighlighted;
  132. if (customComp != nullptr)
  133. customComp->setHighlighted (shouldBeHighlighted);
  134. if (isHighlighted)
  135. if (auto* handler = getAccessibilityHandler())
  136. handler->grabFocus();
  137. repaint();
  138. }
  139. }
  140. static bool isAccessibilityHandlerRequired (const PopupMenu::Item& item)
  141. {
  142. return item.isSectionHeader || hasActiveSubMenu (item) || canBeTriggered (item);
  143. }
  144. PopupMenu::Item item;
  145. private:
  146. //==============================================================================
  147. class ItemAccessibilityHandler : public AccessibilityHandler
  148. {
  149. public:
  150. explicit ItemAccessibilityHandler (ItemComponent& itemComponentToWrap)
  151. : AccessibilityHandler (itemComponentToWrap,
  152. isAccessibilityHandlerRequired (itemComponentToWrap.item) ? AccessibilityRole::menuItem
  153. : AccessibilityRole::ignored,
  154. getAccessibilityActions (*this, itemComponentToWrap)),
  155. itemComponent (itemComponentToWrap)
  156. {
  157. }
  158. String getTitle() const override
  159. {
  160. return itemComponent.item.text;
  161. }
  162. AccessibleState getCurrentState() const override
  163. {
  164. auto state = AccessibilityHandler::getCurrentState().withSelectable()
  165. .withAccessibleOffscreen();
  166. if (hasActiveSubMenu (itemComponent.item))
  167. {
  168. state = itemComponent.parentWindow.isSubMenuVisible() ? state.withExpandable().withExpanded()
  169. : state.withExpandable().withCollapsed();
  170. }
  171. if (itemComponent.item.isTicked)
  172. state = state.withCheckable().withChecked();
  173. return state.isFocused() ? state.withSelected() : state;
  174. }
  175. private:
  176. static AccessibilityActions getAccessibilityActions (ItemAccessibilityHandler& handler,
  177. ItemComponent& item)
  178. {
  179. auto onFocus = [&item]
  180. {
  181. item.parentWindow.disableTimerUntilMouseMoves();
  182. item.parentWindow.ensureItemComponentIsVisible (item, -1);
  183. item.parentWindow.setCurrentlyHighlightedChild (&item);
  184. };
  185. auto onToggle = [&handler, &item, onFocus]
  186. {
  187. if (handler.getCurrentState().isSelected())
  188. item.parentWindow.setCurrentlyHighlightedChild (nullptr);
  189. else
  190. onFocus();
  191. };
  192. auto actions = AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus))
  193. .addAction (AccessibilityActionType::toggle, std::move (onToggle));
  194. if (canBeTriggered (item.item))
  195. {
  196. actions.addAction (AccessibilityActionType::press, [&item]
  197. {
  198. item.parentWindow.setCurrentlyHighlightedChild (&item);
  199. item.parentWindow.triggerCurrentlyHighlightedItem();
  200. });
  201. }
  202. if (hasActiveSubMenu (item.item))
  203. {
  204. auto showSubMenu = [&item]
  205. {
  206. item.parentWindow.showSubMenuFor (&item);
  207. if (auto* subMenu = item.parentWindow.activeSubMenu.get())
  208. subMenu->setCurrentlyHighlightedChild (subMenu->items.getFirst());
  209. };
  210. actions.addAction (AccessibilityActionType::press, showSubMenu);
  211. actions.addAction (AccessibilityActionType::showMenu, showSubMenu);
  212. }
  213. return actions;
  214. }
  215. ItemComponent& itemComponent;
  216. };
  217. std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
  218. {
  219. return item.isSeparator ? nullptr : std::make_unique<ItemAccessibilityHandler> (*this);
  220. }
  221. //==============================================================================
  222. MenuWindow& parentWindow;
  223. const PopupMenu::Options& options;
  224. // NB: we use a copy of the one from the item info in case we're using our own section comp
  225. ReferenceCountedObjectPtr<CustomComponent> customComp;
  226. bool isHighlighted = false;
  227. void updateShortcutKeyDescription()
  228. {
  229. if (item.commandManager != nullptr
  230. && item.itemID != 0
  231. && item.shortcutKeyDescription.isEmpty())
  232. {
  233. String shortcutKey;
  234. for (auto& keypress : item.commandManager->getKeyMappings()
  235. ->getKeyPressesAssignedToCommand (item.itemID))
  236. {
  237. auto key = keypress.getTextDescriptionWithIcons();
  238. if (shortcutKey.isNotEmpty())
  239. shortcutKey << ", ";
  240. if (key.length() == 1 && key[0] < 128)
  241. shortcutKey << "shortcut: '" << key << '\'';
  242. else
  243. shortcutKey << key;
  244. }
  245. item.shortcutKeyDescription = shortcutKey.trim();
  246. }
  247. }
  248. String getTextForMeasurement() const
  249. {
  250. return item.shortcutKeyDescription.isNotEmpty() ? item.text + " " + item.shortcutKeyDescription
  251. : item.text;
  252. }
  253. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent)
  254. };
  255. //==============================================================================
  256. struct MenuWindow : public Component
  257. {
  258. MenuWindow (const PopupMenu& menu, MenuWindow* parentWindow,
  259. Options opts, bool alignToRectangle, bool shouldDismissOnMouseUp,
  260. ApplicationCommandManager** manager, float parentScaleFactor = 1.0f)
  261. : Component ("menu"),
  262. parent (parentWindow),
  263. options (opts.withParentComponent (getLookAndFeel().getParentComponentForMenuOptions (opts))),
  264. managerOfChosenCommand (manager),
  265. componentAttachedTo (options.getTargetComponent()),
  266. dismissOnMouseUp (shouldDismissOnMouseUp),
  267. windowCreationTime (Time::getMillisecondCounter()),
  268. lastFocusedTime (windowCreationTime),
  269. timeEnteredCurrentChildComp (windowCreationTime),
  270. scaleFactor (parentWindow != nullptr ? parentScaleFactor : 1.0f)
  271. {
  272. setWantsKeyboardFocus (false);
  273. setMouseClickGrabsKeyboardFocus (false);
  274. setAlwaysOnTop (true);
  275. setFocusContainerType (FocusContainerType::focusContainer);
  276. setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel())
  277. : menu.lookAndFeel.get());
  278. auto& lf = getLookAndFeel();
  279. if (auto* pc = options.getParentComponent())
  280. {
  281. pc->addChildComponent (this);
  282. }
  283. else
  284. {
  285. const auto shouldDisableAccessibility = [this]
  286. {
  287. const auto* compToCheck = parent != nullptr ? parent
  288. : options.getTargetComponent();
  289. return compToCheck != nullptr && ! compToCheck->isAccessible();
  290. }();
  291. if (shouldDisableAccessibility)
  292. setAccessible (false);
  293. addToDesktop (ComponentPeer::windowIsTemporary
  294. | ComponentPeer::windowIgnoresKeyPresses
  295. | lf.getMenuWindowFlags());
  296. Desktop::getInstance().addGlobalMouseListener (this);
  297. }
  298. if (options.getParentComponent() == nullptr && parentWindow == nullptr && lf.shouldPopupMenuScaleWithTargetComponent (options))
  299. if (auto* targetComponent = options.getTargetComponent())
  300. scaleFactor = Component::getApproximateScaleFactorForComponent (targetComponent);
  301. setOpaque (lf.findColour (PopupMenu::backgroundColourId).isOpaque()
  302. || ! Desktop::canUseSemiTransparentWindows());
  303. const auto initialSelectedId = options.getInitiallySelectedItemId();
  304. for (int i = 0; i < menu.items.size(); ++i)
  305. {
  306. auto& item = menu.items.getReference (i);
  307. if (i + 1 < menu.items.size() || ! item.isSeparator)
  308. {
  309. auto* child = items.add (new ItemComponent (item, options, *this));
  310. if (initialSelectedId != 0 && item.itemID == initialSelectedId)
  311. setCurrentlyHighlightedChild (child);
  312. }
  313. }
  314. auto targetArea = options.getTargetScreenArea() / scaleFactor;
  315. calculateWindowPos (targetArea, alignToRectangle);
  316. setTopLeftPosition (windowPos.getPosition());
  317. if (auto visibleID = options.getItemThatMustBeVisible())
  318. {
  319. for (auto* item : items)
  320. {
  321. if (item->item.itemID == visibleID)
  322. {
  323. const auto targetPosition = [&]
  324. {
  325. if (auto* pc = options.getParentComponent())
  326. return pc->getLocalPoint (nullptr, targetArea.getTopLeft());
  327. return targetArea.getTopLeft();
  328. }();
  329. auto y = targetPosition.getY() - windowPos.getY();
  330. ensureItemComponentIsVisible (*item, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1);
  331. break;
  332. }
  333. }
  334. }
  335. resizeToBestWindowPos();
  336. getActiveWindows().add (this);
  337. lf.preparePopupMenuWindow (*this);
  338. getMouseState (Desktop::getInstance().getMainMouseSource()); // forces creation of a mouse source watcher for the main mouse
  339. }
  340. ~MenuWindow() override
  341. {
  342. getActiveWindows().removeFirstMatchingValue (this);
  343. Desktop::getInstance().removeGlobalMouseListener (this);
  344. activeSubMenu.reset();
  345. items.clear();
  346. }
  347. //==============================================================================
  348. void paint (Graphics& g) override
  349. {
  350. if (isOpaque())
  351. g.fillAll (Colours::white);
  352. auto& theme = getLookAndFeel();
  353. theme.drawPopupMenuBackgroundWithOptions (g, getWidth(), getHeight(), options);
  354. if (columnWidths.isEmpty())
  355. return;
  356. const auto separatorWidth = theme.getPopupMenuColumnSeparatorWidthWithOptions (options);
  357. const auto border = theme.getPopupMenuBorderSizeWithOptions (options);
  358. auto currentX = 0;
  359. std::for_each (columnWidths.begin(), std::prev (columnWidths.end()), [&] (int width)
  360. {
  361. const Rectangle<int> separator (currentX + width,
  362. border,
  363. separatorWidth,
  364. getHeight() - border * 2);
  365. theme.drawPopupMenuColumnSeparatorWithOptions (g, separator, options);
  366. currentX += width + separatorWidth;
  367. });
  368. }
  369. void paintOverChildren (Graphics& g) override
  370. {
  371. auto& lf = getLookAndFeel();
  372. if (options.getParentComponent())
  373. lf.drawResizableFrame (g, getWidth(), getHeight(),
  374. BorderSize<int> (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options)));
  375. if (canScroll())
  376. {
  377. if (isTopScrollZoneActive())
  378. {
  379. lf.drawPopupMenuUpDownArrowWithOptions (g,
  380. getWidth(),
  381. PopupMenuSettings::scrollZone,
  382. true,
  383. options);
  384. }
  385. if (isBottomScrollZoneActive())
  386. {
  387. g.setOrigin (0, getHeight() - PopupMenuSettings::scrollZone);
  388. lf.drawPopupMenuUpDownArrowWithOptions (g,
  389. getWidth(),
  390. PopupMenuSettings::scrollZone,
  391. false,
  392. options);
  393. }
  394. }
  395. }
  396. //==============================================================================
  397. // hide this and all sub-comps
  398. void hide (const PopupMenu::Item* item, bool makeInvisible)
  399. {
  400. if (isVisible())
  401. {
  402. WeakReference<Component> deletionChecker (this);
  403. activeSubMenu.reset();
  404. currentChild = nullptr;
  405. if (item != nullptr
  406. && item->commandManager != nullptr
  407. && item->itemID != 0)
  408. {
  409. *managerOfChosenCommand = item->commandManager;
  410. }
  411. auto resultID = options.hasWatchedComponentBeenDeleted() ? 0 : getResultItemID (item);
  412. exitModalState (resultID);
  413. if (deletionChecker != nullptr)
  414. {
  415. exitingModalState = true;
  416. if (makeInvisible)
  417. setVisible (false);
  418. }
  419. if (resultID != 0
  420. && item != nullptr
  421. && item->action != nullptr)
  422. MessageManager::callAsync (item->action);
  423. }
  424. }
  425. static int getResultItemID (const PopupMenu::Item* item)
  426. {
  427. if (item == nullptr)
  428. return 0;
  429. if (auto* cc = item->customCallback.get())
  430. if (! cc->menuItemTriggered())
  431. return 0;
  432. return item->itemID;
  433. }
  434. void dismissMenu (const PopupMenu::Item* item)
  435. {
  436. if (parent != nullptr)
  437. {
  438. parent->dismissMenu (item);
  439. }
  440. else
  441. {
  442. if (item != nullptr)
  443. {
  444. // need a copy of this on the stack as the one passed in will get deleted during this call
  445. auto mi (*item);
  446. hide (&mi, false);
  447. }
  448. else
  449. {
  450. hide (nullptr, true);
  451. }
  452. }
  453. }
  454. float getDesktopScaleFactor() const override { return scaleFactor * Desktop::getInstance().getGlobalScaleFactor(); }
  455. void visibilityChanged() override
  456. {
  457. if (! isShowing())
  458. return;
  459. auto* accessibleFocus = [this]
  460. {
  461. if (currentChild != nullptr)
  462. if (auto* childHandler = currentChild->getAccessibilityHandler())
  463. return childHandler;
  464. return getAccessibilityHandler();
  465. }();
  466. if (accessibleFocus != nullptr)
  467. accessibleFocus->grabFocus();
  468. }
  469. //==============================================================================
  470. bool keyPressed (const KeyPress& key) override
  471. {
  472. if (key.isKeyCode (KeyPress::downKey))
  473. {
  474. selectNextItem (MenuSelectionDirection::forwards);
  475. }
  476. else if (key.isKeyCode (KeyPress::upKey))
  477. {
  478. selectNextItem (MenuSelectionDirection::backwards);
  479. }
  480. else if (key.isKeyCode (KeyPress::leftKey))
  481. {
  482. if (parent != nullptr)
  483. {
  484. Component::SafePointer<MenuWindow> parentWindow (parent);
  485. ItemComponent* currentChildOfParent = parentWindow->currentChild;
  486. hide (nullptr, true);
  487. if (parentWindow != nullptr)
  488. parentWindow->setCurrentlyHighlightedChild (currentChildOfParent);
  489. disableTimerUntilMouseMoves();
  490. }
  491. else if (componentAttachedTo != nullptr)
  492. {
  493. componentAttachedTo->keyPressed (key);
  494. }
  495. }
  496. else if (key.isKeyCode (KeyPress::rightKey))
  497. {
  498. disableTimerUntilMouseMoves();
  499. if (showSubMenuFor (currentChild))
  500. {
  501. if (isSubMenuVisible())
  502. activeSubMenu->selectNextItem (MenuSelectionDirection::current);
  503. }
  504. else if (componentAttachedTo != nullptr)
  505. {
  506. componentAttachedTo->keyPressed (key);
  507. }
  508. }
  509. else if (key.isKeyCode (KeyPress::returnKey) || key.isKeyCode (KeyPress::spaceKey))
  510. {
  511. triggerCurrentlyHighlightedItem();
  512. }
  513. else if (key.isKeyCode (KeyPress::escapeKey))
  514. {
  515. dismissMenu (nullptr);
  516. }
  517. else
  518. {
  519. return false;
  520. }
  521. return true;
  522. }
  523. void inputAttemptWhenModal() override
  524. {
  525. WeakReference<Component> deletionChecker (this);
  526. for (auto* ms : mouseSourceStates)
  527. {
  528. ms->timerCallback();
  529. if (deletionChecker == nullptr)
  530. return;
  531. }
  532. if (! isOverAnyMenu())
  533. {
  534. if (componentAttachedTo != nullptr)
  535. {
  536. // we want to dismiss the menu, but if we do it synchronously, then
  537. // the mouse-click will be allowed to pass through. That's good, except
  538. // when the user clicks on the button that originally popped the menu up,
  539. // as they'll expect the menu to go away, and in fact it'll just
  540. // come back. So only dismiss synchronously if they're not on the original
  541. // comp that we're attached to.
  542. auto mousePos = componentAttachedTo->getMouseXYRelative();
  543. if (componentAttachedTo->reallyContains (mousePos, true))
  544. {
  545. postCommandMessage (PopupMenuSettings::dismissCommandId); // dismiss asynchronously
  546. return;
  547. }
  548. }
  549. dismissMenu (nullptr);
  550. }
  551. }
  552. void handleCommandMessage (int commandId) override
  553. {
  554. Component::handleCommandMessage (commandId);
  555. if (commandId == PopupMenuSettings::dismissCommandId)
  556. dismissMenu (nullptr);
  557. }
  558. //==============================================================================
  559. void mouseMove (const MouseEvent& e) override { handleMouseEvent (e); }
  560. void mouseDown (const MouseEvent& e) override { handleMouseEvent (e); }
  561. void mouseDrag (const MouseEvent& e) override { handleMouseEvent (e); }
  562. void mouseUp (const MouseEvent& e) override { handleMouseEvent (e); }
  563. void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
  564. {
  565. alterChildYPos (roundToInt (-10.0f * wheel.deltaY * PopupMenuSettings::scrollZone));
  566. }
  567. void handleMouseEvent (const MouseEvent& e)
  568. {
  569. getMouseState (e.source).handleMouseEvent (e);
  570. }
  571. bool windowIsStillValid()
  572. {
  573. if (! isVisible())
  574. return false;
  575. if (componentAttachedTo != options.getTargetComponent())
  576. {
  577. dismissMenu (nullptr);
  578. return false;
  579. }
  580. if (auto* currentlyModalWindow = dynamic_cast<MenuWindow*> (Component::getCurrentlyModalComponent()))
  581. if (! treeContains (currentlyModalWindow))
  582. return false;
  583. if (exitingModalState)
  584. return false;
  585. return true;
  586. }
  587. static Array<MenuWindow*>& getActiveWindows()
  588. {
  589. static Array<MenuWindow*> activeMenuWindows;
  590. return activeMenuWindows;
  591. }
  592. MouseSourceState& getMouseState (MouseInputSource source)
  593. {
  594. MouseSourceState* mouseState = nullptr;
  595. for (auto* ms : mouseSourceStates)
  596. {
  597. if (ms->source == source) mouseState = ms;
  598. else if (ms->source.getType() != source.getType()) ms->stopTimer();
  599. }
  600. if (mouseState == nullptr)
  601. {
  602. mouseState = new MouseSourceState (*this, source);
  603. mouseSourceStates.add (mouseState);
  604. }
  605. return *mouseState;
  606. }
  607. //==============================================================================
  608. bool isOverAnyMenu() const
  609. {
  610. return parent != nullptr ? parent->isOverAnyMenu()
  611. : isOverChildren();
  612. }
  613. bool isOverChildren() const
  614. {
  615. return isVisible()
  616. && (isAnyMouseOver() || (activeSubMenu != nullptr && activeSubMenu->isOverChildren()));
  617. }
  618. bool isAnyMouseOver() const
  619. {
  620. for (auto* ms : mouseSourceStates)
  621. if (ms->isOver())
  622. return true;
  623. return false;
  624. }
  625. bool treeContains (const MenuWindow* const window) const noexcept
  626. {
  627. auto* mw = this;
  628. while (mw->parent != nullptr)
  629. mw = mw->parent;
  630. while (mw != nullptr)
  631. {
  632. if (mw == window)
  633. return true;
  634. mw = mw->activeSubMenu.get();
  635. }
  636. return false;
  637. }
  638. bool doesAnyJuceCompHaveFocus()
  639. {
  640. if (! isForegroundOrEmbeddedProcess (componentAttachedTo))
  641. return false;
  642. if (Component::getCurrentlyFocusedComponent() != nullptr)
  643. return true;
  644. for (int i = ComponentPeer::getNumPeers(); --i >= 0;)
  645. {
  646. if (ComponentPeer::getPeer (i)->isFocused())
  647. {
  648. hasAnyJuceCompHadFocus = true;
  649. return true;
  650. }
  651. }
  652. return ! hasAnyJuceCompHadFocus;
  653. }
  654. //==============================================================================
  655. Rectangle<int> getParentArea (Point<int> targetPoint, Component* relativeTo = nullptr)
  656. {
  657. if (relativeTo != nullptr)
  658. targetPoint = relativeTo->localPointToGlobal (targetPoint);
  659. auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (targetPoint * scaleFactor);
  660. auto parentArea = display->safeAreaInsets.subtractedFrom (display->totalArea);
  661. if (auto* pc = options.getParentComponent())
  662. {
  663. return pc->getLocalArea (nullptr,
  664. pc->getScreenBounds()
  665. .reduced (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options))
  666. .getIntersection (parentArea));
  667. }
  668. return parentArea;
  669. }
  670. void calculateWindowPos (Rectangle<int> target, const bool alignToRectangle)
  671. {
  672. auto parentArea = getParentArea (target.getCentre()) / scaleFactor;
  673. if (auto* pc = options.getParentComponent())
  674. target = pc->getLocalArea (nullptr, target).getIntersection (parentArea);
  675. auto maxMenuHeight = parentArea.getHeight() - 24;
  676. int x, y, widthToUse, heightToUse;
  677. layoutMenuItems (parentArea.getWidth() - 24, maxMenuHeight, widthToUse, heightToUse);
  678. if (alignToRectangle)
  679. {
  680. x = target.getX();
  681. auto spaceUnder = parentArea.getBottom() - target.getBottom();
  682. auto spaceOver = target.getY() - parentArea.getY();
  683. auto bufferHeight = 30;
  684. if (options.getPreferredPopupDirection() == Options::PopupDirection::upwards)
  685. y = (heightToUse < spaceOver - bufferHeight || spaceOver >= spaceUnder) ? target.getY() - heightToUse
  686. : target.getBottom();
  687. else
  688. y = (heightToUse < spaceUnder - bufferHeight || spaceUnder >= spaceOver) ? target.getBottom()
  689. : target.getY() - heightToUse;
  690. }
  691. else
  692. {
  693. bool tendTowardsRight = target.getCentreX() < parentArea.getCentreX();
  694. if (parent != nullptr)
  695. {
  696. if (parent->parent != nullptr)
  697. {
  698. const bool parentGoingRight = (parent->getX() + parent->getWidth() / 2
  699. > parent->parent->getX() + parent->parent->getWidth() / 2);
  700. if (parentGoingRight && target.getRight() + widthToUse < parentArea.getRight() - 4)
  701. tendTowardsRight = true;
  702. else if ((! parentGoingRight) && target.getX() > widthToUse + 4)
  703. tendTowardsRight = false;
  704. }
  705. else if (target.getRight() + widthToUse < parentArea.getRight() - 32)
  706. {
  707. tendTowardsRight = true;
  708. }
  709. }
  710. auto biggestSpace = jmax (parentArea.getRight() - target.getRight(),
  711. target.getX() - parentArea.getX()) - 32;
  712. if (biggestSpace < widthToUse)
  713. {
  714. layoutMenuItems (biggestSpace + target.getWidth() / 3, maxMenuHeight, widthToUse, heightToUse);
  715. if (numColumns > 1)
  716. layoutMenuItems (biggestSpace - 4, maxMenuHeight, widthToUse, heightToUse);
  717. tendTowardsRight = (parentArea.getRight() - target.getRight()) >= (target.getX() - parentArea.getX());
  718. }
  719. x = tendTowardsRight ? jmin (parentArea.getRight() - widthToUse - 4, target.getRight())
  720. : jmax (parentArea.getX() + 4, target.getX() - widthToUse);
  721. if (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) == 0) // workaround for dismissing the window on mouse up when border size is 0
  722. x += tendTowardsRight ? 1 : -1;
  723. const auto border = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options);
  724. y = target.getCentreY() > parentArea.getCentreY() ? jmax (parentArea.getY(), target.getBottom() - heightToUse) + border
  725. : target.getY() - border;
  726. }
  727. x = jmax (parentArea.getX() + 1, jmin (parentArea.getRight() - (widthToUse + 6), x));
  728. y = jmax (parentArea.getY() + 1, jmin (parentArea.getBottom() - (heightToUse + 6), y));
  729. windowPos.setBounds (x, y, widthToUse, heightToUse);
  730. // sets this flag if it's big enough to obscure any of its parent menus
  731. hideOnExit = parent != nullptr
  732. && parent->windowPos.intersects (windowPos.expanded (-4, -4));
  733. }
  734. void layoutMenuItems (const int maxMenuW, const int maxMenuH, int& width, int& height)
  735. {
  736. // Ensure we don't try to add an empty column after the final item
  737. if (auto* last = items.getLast())
  738. last->item.shouldBreakAfter = false;
  739. const auto isBreak = [] (const ItemComponent* item) { return item->item.shouldBreakAfter; };
  740. const auto numBreaks = static_cast<int> (std::count_if (items.begin(), items.end(), isBreak));
  741. numColumns = numBreaks + 1;
  742. if (numBreaks == 0)
  743. insertColumnBreaks (maxMenuW, maxMenuH);
  744. workOutManualSize (maxMenuW);
  745. height = jmin (contentHeight, maxMenuH);
  746. needsToScroll = contentHeight > height;
  747. width = updateYPositions();
  748. }
  749. void insertColumnBreaks (const int maxMenuW, const int maxMenuH)
  750. {
  751. numColumns = options.getMinimumNumColumns();
  752. contentHeight = 0;
  753. auto maximumNumColumns = options.getMaximumNumColumns() > 0 ? options.getMaximumNumColumns() : 7;
  754. for (;;)
  755. {
  756. auto totalW = workOutBestSize (maxMenuW);
  757. if (totalW > maxMenuW)
  758. {
  759. numColumns = jmax (1, numColumns - 1);
  760. workOutBestSize (maxMenuW); // to update col widths
  761. break;
  762. }
  763. if (totalW > maxMenuW / 2
  764. || contentHeight < maxMenuH
  765. || numColumns >= maximumNumColumns)
  766. break;
  767. ++numColumns;
  768. }
  769. const auto itemsPerColumn = (items.size() + numColumns - 1) / numColumns;
  770. for (auto i = 0;; i += itemsPerColumn)
  771. {
  772. const auto breakIndex = i + itemsPerColumn - 1;
  773. if (breakIndex >= items.size())
  774. break;
  775. items[breakIndex]->item.shouldBreakAfter = true;
  776. }
  777. if (! items.isEmpty())
  778. (*std::prev (items.end()))->item.shouldBreakAfter = false;
  779. }
  780. int correctColumnWidths (const int maxMenuW)
  781. {
  782. auto totalW = std::accumulate (columnWidths.begin(), columnWidths.end(), 0);
  783. const auto minWidth = jmin (maxMenuW, options.getMinimumWidth());
  784. if (totalW < minWidth)
  785. {
  786. totalW = minWidth;
  787. for (auto& column : columnWidths)
  788. column = totalW / numColumns;
  789. }
  790. return totalW;
  791. }
  792. void workOutManualSize (const int maxMenuW)
  793. {
  794. contentHeight = 0;
  795. columnWidths.clear();
  796. for (auto it = items.begin(), end = items.end(); it != end;)
  797. {
  798. const auto isBreak = [] (const ItemComponent* item) { return item->item.shouldBreakAfter; };
  799. const auto nextBreak = std::find_if (it, end, isBreak);
  800. const auto columnEnd = nextBreak == end ? end : std::next (nextBreak);
  801. const auto getMaxWidth = [] (int acc, const ItemComponent* item) { return jmax (acc, item->getWidth()); };
  802. const auto colW = std::accumulate (it, columnEnd, options.getStandardItemHeight(), getMaxWidth);
  803. const auto adjustedColW = jmin (maxMenuW / jmax (1, numColumns - 2),
  804. colW + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2);
  805. const auto sumHeight = [] (int acc, const ItemComponent* item) { return acc + item->getHeight(); };
  806. const auto colH = std::accumulate (it, columnEnd, 0, sumHeight);
  807. contentHeight = jmax (contentHeight, colH);
  808. columnWidths.add (adjustedColW);
  809. it = columnEnd;
  810. }
  811. contentHeight += getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2;
  812. correctColumnWidths (maxMenuW);
  813. }
  814. int workOutBestSize (const int maxMenuW)
  815. {
  816. contentHeight = 0;
  817. int childNum = 0;
  818. for (int col = 0; col < numColumns; ++col)
  819. {
  820. int colW = options.getStandardItemHeight(), colH = 0;
  821. auto numChildren = jmin (items.size() - childNum,
  822. (items.size() + numColumns - 1) / numColumns);
  823. for (int i = numChildren; --i >= 0;)
  824. {
  825. colW = jmax (colW, items.getUnchecked (childNum + i)->getWidth());
  826. colH += items.getUnchecked (childNum + i)->getHeight();
  827. }
  828. colW = jmin (maxMenuW / jmax (1, numColumns - 2),
  829. colW + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2);
  830. columnWidths.set (col, colW);
  831. contentHeight = jmax (contentHeight, colH);
  832. childNum += numChildren;
  833. }
  834. return correctColumnWidths (maxMenuW);
  835. }
  836. void ensureItemComponentIsVisible (const ItemComponent& itemComp, int wantedY)
  837. {
  838. if (windowPos.getHeight() > PopupMenuSettings::scrollZone * 4)
  839. {
  840. auto currentY = itemComp.getY();
  841. if (wantedY > 0 || currentY < 0 || itemComp.getBottom() > windowPos.getHeight())
  842. {
  843. if (wantedY < 0)
  844. wantedY = jlimit (PopupMenuSettings::scrollZone,
  845. jmax (PopupMenuSettings::scrollZone,
  846. windowPos.getHeight() - (PopupMenuSettings::scrollZone + itemComp.getHeight())),
  847. currentY);
  848. auto parentArea = getParentArea (windowPos.getPosition(), options.getParentComponent()) / scaleFactor;
  849. auto deltaY = wantedY - currentY;
  850. windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()),
  851. jmin (windowPos.getHeight(), parentArea.getHeight()));
  852. auto newY = jlimit (parentArea.getY(),
  853. parentArea.getBottom() - windowPos.getHeight(),
  854. windowPos.getY() + deltaY);
  855. deltaY -= newY - windowPos.getY();
  856. childYOffset -= deltaY;
  857. windowPos.setPosition (windowPos.getX(), newY);
  858. updateYPositions();
  859. }
  860. }
  861. }
  862. void resizeToBestWindowPos()
  863. {
  864. auto r = windowPos;
  865. if (childYOffset < 0)
  866. {
  867. r = r.withTop (r.getY() - childYOffset);
  868. }
  869. else if (childYOffset > 0)
  870. {
  871. auto spaceAtBottom = r.getHeight() - (contentHeight - childYOffset);
  872. if (spaceAtBottom > 0)
  873. r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom);
  874. }
  875. setBounds (r);
  876. updateYPositions();
  877. }
  878. void alterChildYPos (int delta)
  879. {
  880. if (canScroll())
  881. {
  882. childYOffset += delta;
  883. childYOffset = [&]
  884. {
  885. if (delta < 0)
  886. return jmax (childYOffset, 0);
  887. if (delta > 0)
  888. {
  889. const auto limit = contentHeight
  890. - windowPos.getHeight()
  891. + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options);
  892. return jmin (childYOffset, limit);
  893. }
  894. return childYOffset;
  895. }();
  896. updateYPositions();
  897. }
  898. else
  899. {
  900. childYOffset = 0;
  901. }
  902. resizeToBestWindowPos();
  903. repaint();
  904. }
  905. int updateYPositions()
  906. {
  907. const auto separatorWidth = getLookAndFeel().getPopupMenuColumnSeparatorWidthWithOptions (options);
  908. const auto initialY = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options)
  909. - (childYOffset + (getY() - windowPos.getY()));
  910. auto col = 0;
  911. auto x = 0;
  912. auto y = initialY;
  913. for (const auto& item : items)
  914. {
  915. jassert (col < columnWidths.size());
  916. const auto columnWidth = columnWidths[col];
  917. item->setBounds (x, y, columnWidth, item->getHeight());
  918. y += item->getHeight();
  919. if (item->item.shouldBreakAfter)
  920. {
  921. col += 1;
  922. x += columnWidth + separatorWidth;
  923. y = initialY;
  924. }
  925. }
  926. return std::accumulate (columnWidths.begin(), columnWidths.end(), 0)
  927. + (separatorWidth * (columnWidths.size() - 1));
  928. }
  929. void setCurrentlyHighlightedChild (ItemComponent* child)
  930. {
  931. if (currentChild != nullptr)
  932. currentChild->setHighlighted (false);
  933. currentChild = child;
  934. if (currentChild != nullptr)
  935. {
  936. currentChild->setHighlighted (true);
  937. timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter();
  938. }
  939. if (auto* handler = getAccessibilityHandler())
  940. handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged);
  941. }
  942. bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); }
  943. bool showSubMenuFor (ItemComponent* childComp)
  944. {
  945. activeSubMenu.reset();
  946. if (childComp != nullptr
  947. && hasActiveSubMenu (childComp->item))
  948. {
  949. activeSubMenu.reset (new HelperClasses::MenuWindow (*(childComp->item.subMenu), this,
  950. options.withTargetScreenArea (childComp->getScreenBounds())
  951. .withMinimumWidth (0)
  952. .withTargetComponent (nullptr),
  953. false, dismissOnMouseUp, managerOfChosenCommand, scaleFactor));
  954. activeSubMenu->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
  955. activeSubMenu->enterModalState (false);
  956. activeSubMenu->toFront (false);
  957. return true;
  958. }
  959. return false;
  960. }
  961. void triggerCurrentlyHighlightedItem()
  962. {
  963. if (currentChild != nullptr && canBeTriggered (currentChild->item))
  964. {
  965. dismissMenu (&currentChild->item);
  966. }
  967. }
  968. enum class MenuSelectionDirection
  969. {
  970. forwards,
  971. backwards,
  972. current
  973. };
  974. void selectNextItem (MenuSelectionDirection direction)
  975. {
  976. disableTimerUntilMouseMoves();
  977. auto start = [&]
  978. {
  979. auto index = items.indexOf (currentChild);
  980. if (index >= 0)
  981. return index;
  982. return direction == MenuSelectionDirection::backwards ? items.size() - 1
  983. : 0;
  984. }();
  985. auto preIncrement = (direction != MenuSelectionDirection::current && currentChild != nullptr);
  986. for (int i = items.size(); --i >= 0;)
  987. {
  988. if (preIncrement)
  989. start += (direction == MenuSelectionDirection::backwards ? -1 : 1);
  990. if (auto* mic = items.getUnchecked ((start + items.size()) % items.size()))
  991. {
  992. if (canBeTriggered (mic->item) || hasActiveSubMenu (mic->item))
  993. {
  994. setCurrentlyHighlightedChild (mic);
  995. return;
  996. }
  997. }
  998. if (! preIncrement)
  999. preIncrement = true;
  1000. }
  1001. }
  1002. void disableTimerUntilMouseMoves()
  1003. {
  1004. disableMouseMoves = true;
  1005. if (parent != nullptr)
  1006. parent->disableTimerUntilMouseMoves();
  1007. }
  1008. bool canScroll() const noexcept { return childYOffset != 0 || needsToScroll; }
  1009. bool isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; }
  1010. bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); }
  1011. //==============================================================================
  1012. std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
  1013. {
  1014. return std::make_unique<AccessibilityHandler> (*this,
  1015. AccessibilityRole::popupMenu,
  1016. AccessibilityActions().addAction (AccessibilityActionType::focus, [this]
  1017. {
  1018. if (currentChild != nullptr)
  1019. {
  1020. if (auto* handler = currentChild->getAccessibilityHandler())
  1021. handler->grabFocus();
  1022. }
  1023. else
  1024. {
  1025. selectNextItem (MenuSelectionDirection::forwards);
  1026. }
  1027. }));
  1028. }
  1029. //==============================================================================
  1030. MenuWindow* parent;
  1031. const Options options;
  1032. OwnedArray<ItemComponent> items;
  1033. ApplicationCommandManager** managerOfChosenCommand;
  1034. WeakReference<Component> componentAttachedTo;
  1035. Rectangle<int> windowPos;
  1036. bool hasBeenOver = false, needsToScroll = false;
  1037. bool dismissOnMouseUp, hideOnExit = false, disableMouseMoves = false, hasAnyJuceCompHadFocus = false;
  1038. int numColumns = 0, contentHeight = 0, childYOffset = 0;
  1039. Component::SafePointer<ItemComponent> currentChild;
  1040. std::unique_ptr<MenuWindow> activeSubMenu;
  1041. Array<int> columnWidths;
  1042. uint32 windowCreationTime, lastFocusedTime, timeEnteredCurrentChildComp;
  1043. OwnedArray<MouseSourceState> mouseSourceStates;
  1044. float scaleFactor;
  1045. bool exitingModalState = false;
  1046. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow)
  1047. };
  1048. //==============================================================================
  1049. class MouseSourceState : public Timer
  1050. {
  1051. public:
  1052. MouseSourceState (MenuWindow& w, MouseInputSource s)
  1053. : window (w), source (s), lastScrollTime (Time::getMillisecondCounter())
  1054. {
  1055. startTimerHz (20);
  1056. }
  1057. void handleMouseEvent (const MouseEvent& e)
  1058. {
  1059. if (! window.windowIsStillValid())
  1060. return;
  1061. startTimerHz (20);
  1062. handleMousePosition (e.getScreenPosition());
  1063. }
  1064. void timerCallback() override
  1065. {
  1066. #if JUCE_WINDOWS
  1067. // touch and pen devices on Windows send an offscreen mouse move after mouse up events
  1068. // but we don't want to forward these on as they will dismiss the menu
  1069. if ((source.isTouch() || source.isPen()) && ! isValidMousePosition())
  1070. return;
  1071. #endif
  1072. if (window.windowIsStillValid())
  1073. handleMousePosition (source.getScreenPosition().roundToInt());
  1074. }
  1075. bool isOver() const
  1076. {
  1077. return window.reallyContains (window.getLocalPoint (nullptr, source.getScreenPosition()).roundToInt(), true);
  1078. }
  1079. MenuWindow& window;
  1080. MouseInputSource source;
  1081. private:
  1082. Point<int> lastMousePos;
  1083. double scrollAcceleration = 0;
  1084. uint32 lastScrollTime, lastMouseMoveTime = 0;
  1085. bool isDown = false;
  1086. void handleMousePosition (Point<int> globalMousePos)
  1087. {
  1088. auto localMousePos = window.getLocalPoint (nullptr, globalMousePos);
  1089. auto timeNow = Time::getMillisecondCounter();
  1090. if (timeNow > window.timeEnteredCurrentChildComp + 100
  1091. && window.reallyContains (localMousePos, true)
  1092. && window.currentChild != nullptr
  1093. && ! (window.disableMouseMoves || window.isSubMenuVisible()))
  1094. {
  1095. window.showSubMenuFor (window.currentChild);
  1096. }
  1097. highlightItemUnderMouse (globalMousePos, localMousePos, timeNow);
  1098. const bool overScrollArea = scrollIfNecessary (localMousePos, timeNow);
  1099. const bool isOverAny = window.isOverAnyMenu();
  1100. if (window.hideOnExit && window.hasBeenOver && ! isOverAny)
  1101. window.hide (nullptr, true);
  1102. else
  1103. checkButtonState (localMousePos, timeNow, isDown, overScrollArea, isOverAny);
  1104. }
  1105. void checkButtonState (Point<int> localMousePos, const uint32 timeNow,
  1106. const bool wasDown, const bool overScrollArea, const bool isOverAny)
  1107. {
  1108. isDown = window.hasBeenOver
  1109. && (ModifierKeys::currentModifiers.isAnyMouseButtonDown()
  1110. || ComponentPeer::getCurrentModifiersRealtime().isAnyMouseButtonDown());
  1111. if (! window.doesAnyJuceCompHaveFocus())
  1112. {
  1113. if (timeNow > window.lastFocusedTime + 10)
  1114. {
  1115. PopupMenuSettings::menuWasHiddenBecauseOfAppChange = true;
  1116. window.dismissMenu (nullptr);
  1117. // Note: This object may have been deleted by the previous call.
  1118. }
  1119. }
  1120. else if (wasDown && timeNow > window.windowCreationTime + 250
  1121. && ! (isDown || overScrollArea))
  1122. {
  1123. if (window.reallyContains (localMousePos, true))
  1124. window.triggerCurrentlyHighlightedItem();
  1125. else if ((window.hasBeenOver || ! window.dismissOnMouseUp) && ! isOverAny)
  1126. window.dismissMenu (nullptr);
  1127. // Note: This object may have been deleted by the previous call.
  1128. }
  1129. else
  1130. {
  1131. window.lastFocusedTime = timeNow;
  1132. }
  1133. }
  1134. void highlightItemUnderMouse (Point<int> globalMousePos, Point<int> localMousePos, const uint32 timeNow)
  1135. {
  1136. if (globalMousePos != lastMousePos || timeNow > lastMouseMoveTime + 350)
  1137. {
  1138. const auto isMouseOver = window.reallyContains (localMousePos, true);
  1139. if (isMouseOver)
  1140. window.hasBeenOver = true;
  1141. if (lastMousePos.getDistanceFrom (globalMousePos) > 2)
  1142. {
  1143. lastMouseMoveTime = timeNow;
  1144. if (window.disableMouseMoves && isMouseOver)
  1145. window.disableMouseMoves = false;
  1146. }
  1147. if (window.disableMouseMoves || (window.activeSubMenu != nullptr && window.activeSubMenu->isOverChildren()))
  1148. return;
  1149. const bool isMovingTowardsMenu = isMouseOver && globalMousePos != lastMousePos
  1150. && isMovingTowardsSubmenu (globalMousePos);
  1151. lastMousePos = globalMousePos;
  1152. if (! isMovingTowardsMenu)
  1153. {
  1154. auto* c = window.getComponentAt (localMousePos);
  1155. if (c == &window)
  1156. c = nullptr;
  1157. auto* itemUnderMouse = dynamic_cast<ItemComponent*> (c);
  1158. if (itemUnderMouse == nullptr && c != nullptr)
  1159. itemUnderMouse = c->findParentComponentOfClass<ItemComponent>();
  1160. if (itemUnderMouse != window.currentChild
  1161. && (isMouseOver || (window.activeSubMenu == nullptr) || ! window.activeSubMenu->isVisible()))
  1162. {
  1163. if (isMouseOver && (c != nullptr) && (window.activeSubMenu != nullptr))
  1164. window.activeSubMenu->hide (nullptr, true);
  1165. if (! isMouseOver)
  1166. {
  1167. if (! window.hasBeenOver)
  1168. return;
  1169. itemUnderMouse = nullptr;
  1170. }
  1171. window.setCurrentlyHighlightedChild (itemUnderMouse);
  1172. }
  1173. }
  1174. }
  1175. }
  1176. bool isMovingTowardsSubmenu (Point<int> newGlobalPos) const
  1177. {
  1178. if (window.activeSubMenu == nullptr)
  1179. return false;
  1180. // try to intelligently guess whether the user is moving the mouse towards a currently-open
  1181. // submenu. To do this, look at whether the mouse stays inside a triangular region that
  1182. // extends from the last mouse pos to the submenu's rectangle..
  1183. auto itemScreenBounds = window.activeSubMenu->getScreenBounds();
  1184. auto subX = (float) itemScreenBounds.getX();
  1185. auto oldGlobalPos = lastMousePos;
  1186. if (itemScreenBounds.getX() > window.getX())
  1187. {
  1188. oldGlobalPos -= Point<int> (2, 0); // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels
  1189. }
  1190. else
  1191. {
  1192. oldGlobalPos += Point<int> (2, 0);
  1193. subX += (float) itemScreenBounds.getWidth();
  1194. }
  1195. Path areaTowardsSubMenu;
  1196. areaTowardsSubMenu.addTriangle ((float) oldGlobalPos.x, (float) oldGlobalPos.y,
  1197. subX, (float) itemScreenBounds.getY(),
  1198. subX, (float) itemScreenBounds.getBottom());
  1199. return areaTowardsSubMenu.contains (newGlobalPos.toFloat());
  1200. }
  1201. bool scrollIfNecessary (Point<int> localMousePos, const uint32 timeNow)
  1202. {
  1203. if (window.canScroll()
  1204. && isPositiveAndBelow (localMousePos.x, window.getWidth())
  1205. && (isPositiveAndBelow (localMousePos.y, window.getHeight()) || source.isDragging()))
  1206. {
  1207. if (window.isTopScrollZoneActive() && localMousePos.y < PopupMenuSettings::scrollZone)
  1208. return scroll (timeNow, -1);
  1209. if (window.isBottomScrollZoneActive() && localMousePos.y > window.getHeight() - PopupMenuSettings::scrollZone)
  1210. return scroll (timeNow, 1);
  1211. }
  1212. scrollAcceleration = 1.0;
  1213. return false;
  1214. }
  1215. bool scroll (const uint32 timeNow, const int direction)
  1216. {
  1217. if (timeNow > lastScrollTime + 20)
  1218. {
  1219. scrollAcceleration = jmin (4.0, scrollAcceleration * 1.04);
  1220. int amount = 0;
  1221. for (int i = 0; i < window.items.size() && amount == 0; ++i)
  1222. amount = ((int) scrollAcceleration) * window.items.getUnchecked (i)->getHeight();
  1223. window.alterChildYPos (amount * direction);
  1224. lastScrollTime = timeNow;
  1225. }
  1226. return true;
  1227. }
  1228. #if JUCE_WINDOWS
  1229. bool isValidMousePosition()
  1230. {
  1231. auto screenPos = source.getScreenPosition();
  1232. auto localPos = (window.activeSubMenu == nullptr) ? window.getLocalPoint (nullptr, screenPos)
  1233. : window.activeSubMenu->getLocalPoint (nullptr, screenPos);
  1234. if (localPos.x < 0 && localPos.y < 0)
  1235. return false;
  1236. return true;
  1237. }
  1238. #endif
  1239. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MouseSourceState)
  1240. };
  1241. //==============================================================================
  1242. struct NormalComponentWrapper : public PopupMenu::CustomComponent
  1243. {
  1244. NormalComponentWrapper (Component& comp, int w, int h, bool triggerMenuItemAutomaticallyWhenClicked)
  1245. : PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked),
  1246. width (w), height (h)
  1247. {
  1248. addAndMakeVisible (comp);
  1249. }
  1250. void getIdealSize (int& idealWidth, int& idealHeight) override
  1251. {
  1252. idealWidth = width;
  1253. idealHeight = height;
  1254. }
  1255. void resized() override
  1256. {
  1257. if (auto* child = getChildComponent (0))
  1258. child->setBounds (getLocalBounds());
  1259. }
  1260. const int width, height;
  1261. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper)
  1262. };
  1263. };
  1264. //==============================================================================
  1265. PopupMenu::PopupMenu (const PopupMenu& other)
  1266. : items (other.items),
  1267. lookAndFeel (other.lookAndFeel)
  1268. {
  1269. }
  1270. PopupMenu& PopupMenu::operator= (const PopupMenu& other)
  1271. {
  1272. if (this != &other)
  1273. {
  1274. items = other.items;
  1275. lookAndFeel = other.lookAndFeel;
  1276. }
  1277. return *this;
  1278. }
  1279. PopupMenu::PopupMenu (PopupMenu&& other) noexcept
  1280. : items (std::move (other.items)),
  1281. lookAndFeel (std::move (other.lookAndFeel))
  1282. {
  1283. }
  1284. PopupMenu& PopupMenu::operator= (PopupMenu&& other) noexcept
  1285. {
  1286. items = std::move (other.items);
  1287. lookAndFeel = other.lookAndFeel;
  1288. return *this;
  1289. }
  1290. PopupMenu::~PopupMenu() = default;
  1291. void PopupMenu::clear()
  1292. {
  1293. items.clear();
  1294. }
  1295. //==============================================================================
  1296. PopupMenu::Item::Item() = default;
  1297. PopupMenu::Item::Item (String t) : text (std::move (t)), itemID (-1) {}
  1298. PopupMenu::Item::Item (Item&&) = default;
  1299. PopupMenu::Item& PopupMenu::Item::operator= (Item&&) = default;
  1300. PopupMenu::Item::Item (const Item& other)
  1301. : text (other.text),
  1302. itemID (other.itemID),
  1303. action (other.action),
  1304. subMenu (createCopyIfNotNull (other.subMenu.get())),
  1305. image (other.image != nullptr ? other.image->createCopy() : nullptr),
  1306. customComponent (other.customComponent),
  1307. customCallback (other.customCallback),
  1308. commandManager (other.commandManager),
  1309. shortcutKeyDescription (other.shortcutKeyDescription),
  1310. colour (other.colour),
  1311. isEnabled (other.isEnabled),
  1312. isTicked (other.isTicked),
  1313. isSeparator (other.isSeparator),
  1314. isSectionHeader (other.isSectionHeader),
  1315. shouldBreakAfter (other.shouldBreakAfter)
  1316. {}
  1317. PopupMenu::Item& PopupMenu::Item::operator= (const Item& other)
  1318. {
  1319. text = other.text;
  1320. itemID = other.itemID;
  1321. action = other.action;
  1322. subMenu.reset (createCopyIfNotNull (other.subMenu.get()));
  1323. image = other.image != nullptr ? other.image->createCopy() : std::unique_ptr<Drawable>();
  1324. customComponent = other.customComponent;
  1325. customCallback = other.customCallback;
  1326. commandManager = other.commandManager;
  1327. shortcutKeyDescription = other.shortcutKeyDescription;
  1328. colour = other.colour;
  1329. isEnabled = other.isEnabled;
  1330. isTicked = other.isTicked;
  1331. isSeparator = other.isSeparator;
  1332. isSectionHeader = other.isSectionHeader;
  1333. shouldBreakAfter = other.shouldBreakAfter;
  1334. return *this;
  1335. }
  1336. PopupMenu::Item& PopupMenu::Item::setTicked (bool shouldBeTicked) & noexcept
  1337. {
  1338. isTicked = shouldBeTicked;
  1339. return *this;
  1340. }
  1341. PopupMenu::Item& PopupMenu::Item::setEnabled (bool shouldBeEnabled) & noexcept
  1342. {
  1343. isEnabled = shouldBeEnabled;
  1344. return *this;
  1345. }
  1346. PopupMenu::Item& PopupMenu::Item::setAction (std::function<void()> newAction) & noexcept
  1347. {
  1348. action = std::move (newAction);
  1349. return *this;
  1350. }
  1351. PopupMenu::Item& PopupMenu::Item::setID (int newID) & noexcept
  1352. {
  1353. itemID = newID;
  1354. return *this;
  1355. }
  1356. PopupMenu::Item& PopupMenu::Item::setColour (Colour newColour) & noexcept
  1357. {
  1358. colour = newColour;
  1359. return *this;
  1360. }
  1361. PopupMenu::Item& PopupMenu::Item::setCustomComponent (ReferenceCountedObjectPtr<CustomComponent> comp) & noexcept
  1362. {
  1363. customComponent = comp;
  1364. return *this;
  1365. }
  1366. PopupMenu::Item& PopupMenu::Item::setImage (std::unique_ptr<Drawable> newImage) & noexcept
  1367. {
  1368. image = std::move (newImage);
  1369. return *this;
  1370. }
  1371. PopupMenu::Item&& PopupMenu::Item::setTicked (bool shouldBeTicked) && noexcept
  1372. {
  1373. isTicked = shouldBeTicked;
  1374. return std::move (*this);
  1375. }
  1376. PopupMenu::Item&& PopupMenu::Item::setEnabled (bool shouldBeEnabled) && noexcept
  1377. {
  1378. isEnabled = shouldBeEnabled;
  1379. return std::move (*this);
  1380. }
  1381. PopupMenu::Item&& PopupMenu::Item::setAction (std::function<void()> newAction) && noexcept
  1382. {
  1383. action = std::move (newAction);
  1384. return std::move (*this);
  1385. }
  1386. PopupMenu::Item&& PopupMenu::Item::setID (int newID) && noexcept
  1387. {
  1388. itemID = newID;
  1389. return std::move (*this);
  1390. }
  1391. PopupMenu::Item&& PopupMenu::Item::setColour (Colour newColour) && noexcept
  1392. {
  1393. colour = newColour;
  1394. return std::move (*this);
  1395. }
  1396. PopupMenu::Item&& PopupMenu::Item::setCustomComponent (ReferenceCountedObjectPtr<CustomComponent> comp) && noexcept
  1397. {
  1398. customComponent = comp;
  1399. return std::move (*this);
  1400. }
  1401. PopupMenu::Item&& PopupMenu::Item::setImage (std::unique_ptr<Drawable> newImage) && noexcept
  1402. {
  1403. image = std::move (newImage);
  1404. return std::move (*this);
  1405. }
  1406. void PopupMenu::addItem (Item newItem)
  1407. {
  1408. // An ID of 0 is used as a return value to indicate that the user
  1409. // didn't pick anything, so you shouldn't use it as the ID for an item.
  1410. jassert (newItem.itemID != 0
  1411. || newItem.isSeparator || newItem.isSectionHeader
  1412. || newItem.subMenu != nullptr);
  1413. items.add (std::move (newItem));
  1414. }
  1415. void PopupMenu::addItem (String itemText, std::function<void()> action)
  1416. {
  1417. addItem (std::move (itemText), true, false, std::move (action));
  1418. }
  1419. void PopupMenu::addItem (String itemText, bool isActive, bool isTicked, std::function<void()> action)
  1420. {
  1421. Item i (std::move (itemText));
  1422. i.action = std::move (action);
  1423. i.isEnabled = isActive;
  1424. i.isTicked = isTicked;
  1425. addItem (std::move (i));
  1426. }
  1427. void PopupMenu::addItem (int itemResultID, String itemText, bool isActive, bool isTicked)
  1428. {
  1429. Item i (std::move (itemText));
  1430. i.itemID = itemResultID;
  1431. i.isEnabled = isActive;
  1432. i.isTicked = isTicked;
  1433. addItem (std::move (i));
  1434. }
  1435. static std::unique_ptr<Drawable> createDrawableFromImage (const Image& im)
  1436. {
  1437. if (im.isValid())
  1438. {
  1439. auto d = new DrawableImage();
  1440. d->setImage (im);
  1441. return std::unique_ptr<Drawable> (d);
  1442. }
  1443. return {};
  1444. }
  1445. void PopupMenu::addItem (int itemResultID, String itemText, bool isActive, bool isTicked, const Image& iconToUse)
  1446. {
  1447. addItem (itemResultID, std::move (itemText), isActive, isTicked, createDrawableFromImage (iconToUse));
  1448. }
  1449. void PopupMenu::addItem (int itemResultID, String itemText, bool isActive,
  1450. bool isTicked, std::unique_ptr<Drawable> iconToUse)
  1451. {
  1452. Item i (std::move (itemText));
  1453. i.itemID = itemResultID;
  1454. i.isEnabled = isActive;
  1455. i.isTicked = isTicked;
  1456. i.image = std::move (iconToUse);
  1457. addItem (std::move (i));
  1458. }
  1459. void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager,
  1460. const CommandID commandID,
  1461. String displayName,
  1462. std::unique_ptr<Drawable> iconToUse)
  1463. {
  1464. jassert (commandManager != nullptr && commandID != 0);
  1465. if (auto* registeredInfo = commandManager->getCommandForID (commandID))
  1466. {
  1467. ApplicationCommandInfo info (*registeredInfo);
  1468. auto* target = commandManager->getTargetForCommand (commandID, info);
  1469. Item i;
  1470. i.text = displayName.isNotEmpty() ? std::move (displayName) : info.shortName;
  1471. i.itemID = (int) commandID;
  1472. i.commandManager = commandManager;
  1473. i.isEnabled = target != nullptr && (info.flags & ApplicationCommandInfo::isDisabled) == 0;
  1474. i.isTicked = (info.flags & ApplicationCommandInfo::isTicked) != 0;
  1475. i.image = std::move (iconToUse);
  1476. addItem (std::move (i));
  1477. }
  1478. }
  1479. void PopupMenu::addColouredItem (int itemResultID, String itemText, Colour itemTextColour,
  1480. bool isActive, bool isTicked, std::unique_ptr<Drawable> iconToUse)
  1481. {
  1482. Item i (std::move (itemText));
  1483. i.itemID = itemResultID;
  1484. i.colour = itemTextColour;
  1485. i.isEnabled = isActive;
  1486. i.isTicked = isTicked;
  1487. i.image = std::move (iconToUse);
  1488. addItem (std::move (i));
  1489. }
  1490. void PopupMenu::addColouredItem (int itemResultID, String itemText, Colour itemTextColour,
  1491. bool isActive, bool isTicked, const Image& iconToUse)
  1492. {
  1493. Item i (std::move (itemText));
  1494. i.itemID = itemResultID;
  1495. i.colour = itemTextColour;
  1496. i.isEnabled = isActive;
  1497. i.isTicked = isTicked;
  1498. i.image = createDrawableFromImage (iconToUse);
  1499. addItem (std::move (i));
  1500. }
  1501. void PopupMenu::addCustomItem (int itemResultID,
  1502. std::unique_ptr<CustomComponent> cc,
  1503. std::unique_ptr<const PopupMenu> subMenu,
  1504. const String& itemTitle)
  1505. {
  1506. Item i;
  1507. i.text = itemTitle;
  1508. i.itemID = itemResultID;
  1509. i.customComponent = cc.release();
  1510. i.subMenu.reset (createCopyIfNotNull (subMenu.get()));
  1511. // If this assertion is hit, this item will be visible to screen readers but with
  1512. // no name, which may be confusing to users.
  1513. // It's probably a good idea to add a title for this menu item that describes
  1514. // the meaning of the item, or the contents of the submenu, as appropriate.
  1515. // If you don't want this menu item to be press-able directly, pass "false" to the
  1516. // constructor of the CustomComponent.
  1517. jassert (! (HelperClasses::ItemComponent::isAccessibilityHandlerRequired (i) && itemTitle.isEmpty()));
  1518. addItem (std::move (i));
  1519. }
  1520. void PopupMenu::addCustomItem (int itemResultID,
  1521. Component& customComponent,
  1522. int idealWidth, int idealHeight,
  1523. bool triggerMenuItemAutomaticallyWhenClicked,
  1524. std::unique_ptr<const PopupMenu> subMenu,
  1525. const String& itemTitle)
  1526. {
  1527. auto comp = std::make_unique<HelperClasses::NormalComponentWrapper> (customComponent, idealWidth, idealHeight,
  1528. triggerMenuItemAutomaticallyWhenClicked);
  1529. addCustomItem (itemResultID, std::move (comp), std::move (subMenu), itemTitle);
  1530. }
  1531. void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive)
  1532. {
  1533. addSubMenu (std::move (subMenuName), std::move (subMenu), isActive, nullptr, false, 0);
  1534. }
  1535. void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive,
  1536. const Image& iconToUse, bool isTicked, int itemResultID)
  1537. {
  1538. addSubMenu (std::move (subMenuName), std::move (subMenu), isActive,
  1539. createDrawableFromImage (iconToUse), isTicked, itemResultID);
  1540. }
  1541. void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive,
  1542. std::unique_ptr<Drawable> iconToUse, bool isTicked, int itemResultID)
  1543. {
  1544. Item i (std::move (subMenuName));
  1545. i.itemID = itemResultID;
  1546. i.isEnabled = isActive && (itemResultID != 0 || subMenu.getNumItems() > 0);
  1547. i.subMenu.reset (new PopupMenu (std::move (subMenu)));
  1548. i.isTicked = isTicked;
  1549. i.image = std::move (iconToUse);
  1550. addItem (std::move (i));
  1551. }
  1552. void PopupMenu::addSeparator()
  1553. {
  1554. if (items.size() > 0 && ! items.getLast().isSeparator)
  1555. {
  1556. Item i;
  1557. i.isSeparator = true;
  1558. addItem (std::move (i));
  1559. }
  1560. }
  1561. void PopupMenu::addSectionHeader (String title)
  1562. {
  1563. Item i (std::move (title));
  1564. i.itemID = 0;
  1565. i.isSectionHeader = true;
  1566. addItem (std::move (i));
  1567. }
  1568. void PopupMenu::addColumnBreak()
  1569. {
  1570. if (! items.isEmpty())
  1571. std::prev (items.end())->shouldBreakAfter = true;
  1572. }
  1573. //==============================================================================
  1574. PopupMenu::Options::Options()
  1575. {
  1576. targetArea.setPosition (Desktop::getMousePosition());
  1577. }
  1578. template <typename Member, typename Item>
  1579. static PopupMenu::Options with (PopupMenu::Options options, Member&& member, Item&& item)
  1580. {
  1581. options.*member = std::forward<Item> (item);
  1582. return options;
  1583. }
  1584. PopupMenu::Options PopupMenu::Options::withTargetComponent (Component* comp) const
  1585. {
  1586. auto o = with (*this, &Options::targetComponent, comp);
  1587. if (comp != nullptr)
  1588. o.targetArea = comp->getScreenBounds();
  1589. return o;
  1590. }
  1591. PopupMenu::Options PopupMenu::Options::withTargetComponent (Component& comp) const
  1592. {
  1593. return withTargetComponent (&comp);
  1594. }
  1595. PopupMenu::Options PopupMenu::Options::withTargetScreenArea (Rectangle<int> area) const
  1596. {
  1597. return with (*this, &Options::targetArea, area);
  1598. }
  1599. PopupMenu::Options PopupMenu::Options::withMousePosition() const
  1600. {
  1601. return withTargetScreenArea (Rectangle<int>{}.withPosition (Desktop::getMousePosition()));
  1602. }
  1603. PopupMenu::Options PopupMenu::Options::withDeletionCheck (Component& comp) const
  1604. {
  1605. return with (with (*this, &Options::isWatchingForDeletion, true),
  1606. &Options::componentToWatchForDeletion,
  1607. &comp);
  1608. }
  1609. PopupMenu::Options PopupMenu::Options::withMinimumWidth (int w) const
  1610. {
  1611. return with (*this, &Options::minWidth, w);
  1612. }
  1613. PopupMenu::Options PopupMenu::Options::withMinimumNumColumns (int cols) const
  1614. {
  1615. return with (*this, &Options::minColumns, cols);
  1616. }
  1617. PopupMenu::Options PopupMenu::Options::withMaximumNumColumns (int cols) const
  1618. {
  1619. return with (*this, &Options::maxColumns, cols);
  1620. }
  1621. PopupMenu::Options PopupMenu::Options::withStandardItemHeight (int height) const
  1622. {
  1623. return with (*this, &Options::standardHeight, height);
  1624. }
  1625. PopupMenu::Options PopupMenu::Options::withItemThatMustBeVisible (int idOfItemToBeVisible) const
  1626. {
  1627. return with (*this, &Options::visibleItemID, idOfItemToBeVisible);
  1628. }
  1629. PopupMenu::Options PopupMenu::Options::withParentComponent (Component* parent) const
  1630. {
  1631. return with (*this, &Options::parentComponent, parent);
  1632. }
  1633. PopupMenu::Options PopupMenu::Options::withPreferredPopupDirection (PopupDirection direction) const
  1634. {
  1635. return with (*this, &Options::preferredPopupDirection, direction);
  1636. }
  1637. PopupMenu::Options PopupMenu::Options::withInitiallySelectedItem (int idOfItemToBeSelected) const
  1638. {
  1639. return with (*this, &Options::initiallySelectedItemId, idOfItemToBeSelected);
  1640. }
  1641. Component* PopupMenu::createWindow (const Options& options,
  1642. ApplicationCommandManager** managerOfChosenCommand) const
  1643. {
  1644. #if JUCE_WINDOWS
  1645. const auto scope = [&]() -> std::unique_ptr<ScopedThreadDPIAwarenessSetter>
  1646. {
  1647. if (auto* target = options.getTargetComponent())
  1648. if (auto* handle = target->getWindowHandle())
  1649. return std::make_unique<ScopedThreadDPIAwarenessSetter> (handle);
  1650. return nullptr;
  1651. }();
  1652. #endif
  1653. return items.isEmpty() ? nullptr
  1654. : new HelperClasses::MenuWindow (*this, nullptr, options,
  1655. ! options.getTargetScreenArea().isEmpty(),
  1656. ModifierKeys::currentModifiers.isAnyMouseButtonDown(),
  1657. managerOfChosenCommand);
  1658. }
  1659. //==============================================================================
  1660. // This invokes any command manager commands and deletes the menu window when it is dismissed
  1661. struct PopupMenuCompletionCallback : public ModalComponentManager::Callback
  1662. {
  1663. PopupMenuCompletionCallback() = default;
  1664. void modalStateFinished (int result) override
  1665. {
  1666. if (managerOfChosenCommand != nullptr && result != 0)
  1667. {
  1668. ApplicationCommandTarget::InvocationInfo info (result);
  1669. info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
  1670. managerOfChosenCommand->invoke (info, true);
  1671. }
  1672. // (this would be the place to fade out the component, if that's what's required)
  1673. component.reset();
  1674. if (PopupMenuSettings::menuWasHiddenBecauseOfAppChange)
  1675. return;
  1676. if (auto* focusComponent = Component::getCurrentlyFocusedComponent())
  1677. {
  1678. const auto focusedIsNotMinimised = [focusComponent]
  1679. {
  1680. if (auto* peer = focusComponent->getPeer())
  1681. return ! peer->isMinimised();
  1682. return false;
  1683. }();
  1684. if (focusedIsNotMinimised)
  1685. {
  1686. if (auto* topLevel = focusComponent->getTopLevelComponent())
  1687. topLevel->toFront (true);
  1688. if (focusComponent->isShowing() && ! focusComponent->hasKeyboardFocus (true))
  1689. focusComponent->grabKeyboardFocus();
  1690. }
  1691. }
  1692. }
  1693. ApplicationCommandManager* managerOfChosenCommand = nullptr;
  1694. std::unique_ptr<Component> component;
  1695. JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback)
  1696. };
  1697. int PopupMenu::showWithOptionalCallback (const Options& options,
  1698. ModalComponentManager::Callback* userCallback,
  1699. bool canBeModal)
  1700. {
  1701. std::unique_ptr<ModalComponentManager::Callback> userCallbackDeleter (userCallback);
  1702. std::unique_ptr<PopupMenuCompletionCallback> callback (new PopupMenuCompletionCallback());
  1703. if (auto* window = createWindow (options, &(callback->managerOfChosenCommand)))
  1704. {
  1705. callback->component.reset (window);
  1706. PopupMenuSettings::menuWasHiddenBecauseOfAppChange = false;
  1707. window->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
  1708. window->enterModalState (false, userCallbackDeleter.release());
  1709. ModalComponentManager::getInstance()->attachCallback (window, callback.release());
  1710. window->toFront (false); // need to do this after making it modal, or it could
  1711. // be stuck behind other comps that are already modal..
  1712. #if JUCE_MODAL_LOOPS_PERMITTED
  1713. if (userCallback == nullptr && canBeModal)
  1714. return window->runModalLoop();
  1715. #else
  1716. ignoreUnused (canBeModal);
  1717. jassert (! (userCallback == nullptr && canBeModal));
  1718. #endif
  1719. }
  1720. return 0;
  1721. }
  1722. //==============================================================================
  1723. #if JUCE_MODAL_LOOPS_PERMITTED
  1724. int PopupMenu::showMenu (const Options& options)
  1725. {
  1726. return showWithOptionalCallback (options, nullptr, true);
  1727. }
  1728. #endif
  1729. void PopupMenu::showMenuAsync (const Options& options)
  1730. {
  1731. showWithOptionalCallback (options, nullptr, false);
  1732. }
  1733. void PopupMenu::showMenuAsync (const Options& options, ModalComponentManager::Callback* userCallback)
  1734. {
  1735. #if ! JUCE_MODAL_LOOPS_PERMITTED
  1736. jassert (userCallback != nullptr);
  1737. #endif
  1738. showWithOptionalCallback (options, userCallback, false);
  1739. }
  1740. void PopupMenu::showMenuAsync (const Options& options, std::function<void (int)> userCallback)
  1741. {
  1742. showWithOptionalCallback (options, ModalCallbackFunction::create (userCallback), false);
  1743. }
  1744. //==============================================================================
  1745. #if JUCE_MODAL_LOOPS_PERMITTED
  1746. int PopupMenu::show (int itemIDThatMustBeVisible, int minimumWidth,
  1747. int maximumNumColumns, int standardItemHeight,
  1748. ModalComponentManager::Callback* callback)
  1749. {
  1750. return showWithOptionalCallback (Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
  1751. .withMinimumWidth (minimumWidth)
  1752. .withMaximumNumColumns (maximumNumColumns)
  1753. .withStandardItemHeight (standardItemHeight),
  1754. callback, true);
  1755. }
  1756. int PopupMenu::showAt (Rectangle<int> screenAreaToAttachTo,
  1757. int itemIDThatMustBeVisible, int minimumWidth,
  1758. int maximumNumColumns, int standardItemHeight,
  1759. ModalComponentManager::Callback* callback)
  1760. {
  1761. return showWithOptionalCallback (Options().withTargetScreenArea (screenAreaToAttachTo)
  1762. .withItemThatMustBeVisible (itemIDThatMustBeVisible)
  1763. .withMinimumWidth (minimumWidth)
  1764. .withMaximumNumColumns (maximumNumColumns)
  1765. .withStandardItemHeight (standardItemHeight),
  1766. callback, true);
  1767. }
  1768. int PopupMenu::showAt (Component* componentToAttachTo,
  1769. int itemIDThatMustBeVisible, int minimumWidth,
  1770. int maximumNumColumns, int standardItemHeight,
  1771. ModalComponentManager::Callback* callback)
  1772. {
  1773. auto options = Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
  1774. .withMinimumWidth (minimumWidth)
  1775. .withMaximumNumColumns (maximumNumColumns)
  1776. .withStandardItemHeight (standardItemHeight);
  1777. if (componentToAttachTo != nullptr)
  1778. options = options.withTargetComponent (componentToAttachTo);
  1779. return showWithOptionalCallback (options, callback, true);
  1780. }
  1781. #endif
  1782. bool JUCE_CALLTYPE PopupMenu::dismissAllActiveMenus()
  1783. {
  1784. auto& windows = HelperClasses::MenuWindow::getActiveWindows();
  1785. auto numWindows = windows.size();
  1786. for (int i = numWindows; --i >= 0;)
  1787. {
  1788. if (auto* pmw = windows[i])
  1789. {
  1790. pmw->setLookAndFeel (nullptr);
  1791. pmw->dismissMenu (nullptr);
  1792. }
  1793. }
  1794. return numWindows > 0;
  1795. }
  1796. //==============================================================================
  1797. int PopupMenu::getNumItems() const noexcept
  1798. {
  1799. int num = 0;
  1800. for (auto& mi : items)
  1801. if (! mi.isSeparator)
  1802. ++num;
  1803. return num;
  1804. }
  1805. bool PopupMenu::containsCommandItem (const int commandID) const
  1806. {
  1807. for (auto& mi : items)
  1808. if ((mi.itemID == commandID && mi.commandManager != nullptr)
  1809. || (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID)))
  1810. return true;
  1811. return false;
  1812. }
  1813. bool PopupMenu::containsAnyActiveItems() const noexcept
  1814. {
  1815. for (auto& mi : items)
  1816. {
  1817. if (mi.subMenu != nullptr)
  1818. {
  1819. if (mi.subMenu->containsAnyActiveItems())
  1820. return true;
  1821. }
  1822. else if (mi.isEnabled)
  1823. {
  1824. return true;
  1825. }
  1826. }
  1827. return false;
  1828. }
  1829. void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel)
  1830. {
  1831. lookAndFeel = newLookAndFeel;
  1832. }
  1833. void PopupMenu::setItem (CustomComponent& c, const Item* itemToUse)
  1834. {
  1835. c.item = itemToUse;
  1836. c.repaint();
  1837. }
  1838. //==============================================================================
  1839. PopupMenu::CustomComponent::CustomComponent() : CustomComponent (true) {}
  1840. PopupMenu::CustomComponent::CustomComponent (bool autoTrigger)
  1841. : triggeredAutomatically (autoTrigger)
  1842. {
  1843. }
  1844. void PopupMenu::CustomComponent::setHighlighted (bool shouldBeHighlighted)
  1845. {
  1846. isHighlighted = shouldBeHighlighted;
  1847. repaint();
  1848. }
  1849. void PopupMenu::CustomComponent::triggerMenuItem()
  1850. {
  1851. if (auto* mic = findParentComponentOfClass<HelperClasses::ItemComponent>())
  1852. {
  1853. if (auto* pmw = mic->findParentComponentOfClass<HelperClasses::MenuWindow>())
  1854. {
  1855. pmw->dismissMenu (&mic->item);
  1856. }
  1857. else
  1858. {
  1859. // something must have gone wrong with the component hierarchy if this happens..
  1860. jassertfalse;
  1861. }
  1862. }
  1863. else
  1864. {
  1865. // why isn't this component inside a menu? Not much point triggering the item if
  1866. // there's no menu.
  1867. jassertfalse;
  1868. }
  1869. }
  1870. //==============================================================================
  1871. PopupMenu::CustomCallback::CustomCallback() {}
  1872. PopupMenu::CustomCallback::~CustomCallback() {}
  1873. //==============================================================================
  1874. PopupMenu::MenuItemIterator::MenuItemIterator (const PopupMenu& m, bool recurse) : searchRecursively (recurse)
  1875. {
  1876. index.add (0);
  1877. menus.add (&m);
  1878. }
  1879. PopupMenu::MenuItemIterator::~MenuItemIterator() = default;
  1880. bool PopupMenu::MenuItemIterator::next()
  1881. {
  1882. if (index.size() == 0 || menus.getLast()->items.size() == 0)
  1883. return false;
  1884. currentItem = const_cast<PopupMenu::Item*> (&(menus.getLast()->items.getReference (index.getLast())));
  1885. if (searchRecursively && currentItem->subMenu != nullptr)
  1886. {
  1887. index.add (0);
  1888. menus.add (currentItem->subMenu.get());
  1889. }
  1890. else
  1891. {
  1892. index.setUnchecked (index.size() - 1, index.getLast() + 1);
  1893. }
  1894. while (index.size() > 0 && index.getLast() >= (int) menus.getLast()->items.size())
  1895. {
  1896. index.removeLast();
  1897. menus.removeLast();
  1898. if (index.size() > 0)
  1899. index.setUnchecked (index.size() - 1, index.getLast() + 1);
  1900. }
  1901. return true;
  1902. }
  1903. PopupMenu::Item& PopupMenu::MenuItemIterator::getItem() const
  1904. {
  1905. jassert (currentItem != nullptr);
  1906. return *(currentItem);
  1907. }
  1908. void PopupMenu::LookAndFeelMethods::drawPopupMenuBackground (Graphics&, int, int) {}
  1909. void PopupMenu::LookAndFeelMethods::drawPopupMenuItem (Graphics&, const Rectangle<int>&,
  1910. bool, bool, bool,
  1911. bool, bool,
  1912. const String&,
  1913. const String&,
  1914. const Drawable*,
  1915. const Colour*) {}
  1916. void PopupMenu::LookAndFeelMethods::drawPopupMenuSectionHeader (Graphics&, const Rectangle<int>&,
  1917. const String&) {}
  1918. void PopupMenu::LookAndFeelMethods::drawPopupMenuUpDownArrow (Graphics&, int, int, bool) {}
  1919. void PopupMenu::LookAndFeelMethods::getIdealPopupMenuItemSize (const String&, bool, int, int&, int&) {}
  1920. int PopupMenu::LookAndFeelMethods::getPopupMenuBorderSize() { return 0; }
  1921. } // namespace juce