Audio plugin host https://kx.studio/carla
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.

1844 lines
61KB

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