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.

2009 lines
68KB

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