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.

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