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.

1995 lines
67KB

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