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.

1844 lines
62KB

  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. const Array<KeyPress> keyPresses (item.commandManager->getKeyMappings()
  130. ->getKeyPressesAssignedToCommand (item.itemID));
  131. for (int i = 0; i < keyPresses.size(); ++i)
  132. {
  133. const String key (keyPresses.getReference (i).getTextDescriptionWithIcons());
  134. if (shortcutKey.isNotEmpty())
  135. shortcutKey << ", ";
  136. if (key.length() == 1 && key[0] < 128)
  137. shortcutKey << "shortcut: '" << key << '\'';
  138. else
  139. shortcutKey << key;
  140. }
  141. item.shortcutKeyDescription = shortcutKey.trim();
  142. }
  143. }
  144. String getTextForMeasurement() const
  145. {
  146. return item.shortcutKeyDescription.isNotEmpty() ? item.text + " " + item.shortcutKeyDescription
  147. : item.text;
  148. }
  149. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent)
  150. };
  151. //==============================================================================
  152. class MenuWindow : public Component
  153. {
  154. public:
  155. MenuWindow (const PopupMenu& menu, MenuWindow* const parentWindow,
  156. const Options& opts,
  157. const bool alignToRectangle,
  158. const bool shouldDismissOnMouseUp,
  159. ApplicationCommandManager** const manager)
  160. : Component ("menu"),
  161. parent (parentWindow),
  162. options (opts),
  163. managerOfChosenCommand (manager),
  164. componentAttachedTo (options.targetComponent),
  165. parentComponent (nullptr),
  166. hasBeenOver (false),
  167. needsToScroll (false),
  168. dismissOnMouseUp (shouldDismissOnMouseUp),
  169. hideOnExit (false),
  170. disableMouseMoves (false),
  171. hasAnyJuceCompHadFocus (false),
  172. numColumns (0),
  173. contentHeight (0),
  174. childYOffset (0),
  175. windowCreationTime (Time::getMillisecondCounter()),
  176. lastFocusedTime (windowCreationTime),
  177. timeEnteredCurrentChildComp (windowCreationTime)
  178. {
  179. setWantsKeyboardFocus (false);
  180. setMouseClickGrabsKeyboardFocus (false);
  181. setAlwaysOnTop (true);
  182. setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel())
  183. : menu.lookAndFeel.get());
  184. LookAndFeel& lf = getLookAndFeel();
  185. parentComponent = lf.getParentComponentForMenuOptions (options);
  186. setOpaque (lf.findColour (PopupMenu::backgroundColourId).isOpaque()
  187. || ! Desktop::canUseSemiTransparentWindows());
  188. for (int i = 0; i < menu.items.size(); ++i)
  189. {
  190. PopupMenu::Item* const item = menu.items.getUnchecked (i);
  191. if (i < menu.items.size() - 1 || ! item->isSeparator)
  192. items.add (new ItemComponent (*item, options.standardHeight, *this));
  193. }
  194. calculateWindowPos (options.targetArea, alignToRectangle);
  195. setTopLeftPosition (windowPos.getPosition());
  196. updateYPositions();
  197. if (options.visibleItemID != 0)
  198. {
  199. const Point<int> targetPosition =
  200. (parentComponent != nullptr ? parentComponent->getLocalPoint (nullptr, options.targetArea.getTopLeft())
  201. : options.targetArea.getTopLeft());
  202. const int y = targetPosition.getY() - windowPos.getY();
  203. ensureItemIsVisible (options.visibleItemID,
  204. isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1);
  205. }
  206. resizeToBestWindowPos();
  207. if (parentComponent != nullptr)
  208. {
  209. parentComponent->addChildComponent (this);
  210. }
  211. else
  212. {
  213. addToDesktop (ComponentPeer::windowIsTemporary
  214. | ComponentPeer::windowIgnoresKeyPresses
  215. | lf.getMenuWindowFlags());
  216. getActiveWindows().add (this);
  217. Desktop::getInstance().addGlobalMouseListener (this);
  218. }
  219. lf.preparePopupMenuWindow (*this);
  220. }
  221. ~MenuWindow()
  222. {
  223. getActiveWindows().removeFirstMatchingValue (this);
  224. Desktop::getInstance().removeGlobalMouseListener (this);
  225. activeSubMenu = nullptr;
  226. items.clear();
  227. }
  228. //==============================================================================
  229. void paint (Graphics& g) override
  230. {
  231. if (isOpaque())
  232. g.fillAll (Colours::white);
  233. getLookAndFeel().drawPopupMenuBackground (g, getWidth(), getHeight());
  234. }
  235. void paintOverChildren (Graphics& g) override
  236. {
  237. LookAndFeel& lf = getLookAndFeel();
  238. if (parentComponent != nullptr)
  239. lf.drawResizableFrame (g, getWidth(), getHeight(), BorderSize<int> (PopupMenuSettings::borderSize));
  240. if (canScroll())
  241. {
  242. if (isTopScrollZoneActive())
  243. lf.drawPopupMenuUpDownArrow (g, getWidth(), PopupMenuSettings::scrollZone, true);
  244. if (isBottomScrollZoneActive())
  245. {
  246. g.setOrigin (0, getHeight() - PopupMenuSettings::scrollZone);
  247. lf.drawPopupMenuUpDownArrow (g, getWidth(), PopupMenuSettings::scrollZone, false);
  248. }
  249. }
  250. }
  251. //==============================================================================
  252. // hide this and all sub-comps
  253. void hide (const PopupMenu::Item* const item, const bool makeInvisible)
  254. {
  255. if (isVisible())
  256. {
  257. WeakReference<Component> deletionChecker (this);
  258. activeSubMenu = nullptr;
  259. currentChild = nullptr;
  260. if (item != nullptr
  261. && item->commandManager != nullptr
  262. && item->itemID != 0)
  263. {
  264. *managerOfChosenCommand = item->commandManager;
  265. }
  266. exitModalState (getResultItemID (item));
  267. if (makeInvisible && (deletionChecker != nullptr))
  268. setVisible (false);
  269. }
  270. }
  271. static int getResultItemID (const PopupMenu::Item* item)
  272. {
  273. if (item == nullptr)
  274. return 0;
  275. if (CustomCallback* cc = item->customCallback)
  276. if (! cc->menuItemTriggered())
  277. return 0;
  278. return item->itemID;
  279. }
  280. void dismissMenu (const PopupMenu::Item* const item)
  281. {
  282. if (parent != nullptr)
  283. {
  284. parent->dismissMenu (item);
  285. }
  286. else
  287. {
  288. if (item != nullptr)
  289. {
  290. // need a copy of this on the stack as the one passed in will get deleted during this call
  291. const PopupMenu::Item mi (*item);
  292. hide (&mi, false);
  293. }
  294. else
  295. {
  296. hide (nullptr, false);
  297. }
  298. }
  299. }
  300. //==============================================================================
  301. bool keyPressed (const KeyPress& key) override
  302. {
  303. if (key.isKeyCode (KeyPress::downKey))
  304. {
  305. selectNextItem (1);
  306. }
  307. else if (key.isKeyCode (KeyPress::upKey))
  308. {
  309. selectNextItem (-1);
  310. }
  311. else if (key.isKeyCode (KeyPress::leftKey))
  312. {
  313. if (parent != nullptr)
  314. {
  315. Component::SafePointer<MenuWindow> parentWindow (parent);
  316. ItemComponent* currentChildOfParent = parentWindow->currentChild;
  317. hide (nullptr, true);
  318. if (parentWindow != nullptr)
  319. parentWindow->setCurrentlyHighlightedChild (currentChildOfParent);
  320. disableTimerUntilMouseMoves();
  321. }
  322. else if (componentAttachedTo != nullptr)
  323. {
  324. componentAttachedTo->keyPressed (key);
  325. }
  326. }
  327. else if (key.isKeyCode (KeyPress::rightKey))
  328. {
  329. disableTimerUntilMouseMoves();
  330. if (showSubMenuFor (currentChild))
  331. {
  332. if (isSubMenuVisible())
  333. activeSubMenu->selectNextItem (1);
  334. }
  335. else if (componentAttachedTo != nullptr)
  336. {
  337. componentAttachedTo->keyPressed (key);
  338. }
  339. }
  340. else if (key.isKeyCode (KeyPress::returnKey))
  341. {
  342. triggerCurrentlyHighlightedItem();
  343. }
  344. else if (key.isKeyCode (KeyPress::escapeKey))
  345. {
  346. dismissMenu (nullptr);
  347. }
  348. else
  349. {
  350. return false;
  351. }
  352. return true;
  353. }
  354. void inputAttemptWhenModal() override
  355. {
  356. WeakReference<Component> deletionChecker (this);
  357. for (int i = mouseSourceStates.size(); --i >= 0;)
  358. {
  359. mouseSourceStates.getUnchecked (i)->timerCallback();
  360. if (deletionChecker == nullptr)
  361. return;
  362. }
  363. if (! isOverAnyMenu())
  364. {
  365. if (componentAttachedTo != nullptr)
  366. {
  367. // we want to dismiss the menu, but if we do it synchronously, then
  368. // the mouse-click will be allowed to pass through. That's good, except
  369. // when the user clicks on the button that originally popped the menu up,
  370. // as they'll expect the menu to go away, and in fact it'll just
  371. // come back. So only dismiss synchronously if they're not on the original
  372. // comp that we're attached to.
  373. const Point<int> mousePos (componentAttachedTo->getMouseXYRelative());
  374. if (componentAttachedTo->reallyContains (mousePos, true))
  375. {
  376. postCommandMessage (PopupMenuSettings::dismissCommandId); // dismiss asynchrounously
  377. return;
  378. }
  379. }
  380. dismissMenu (nullptr);
  381. }
  382. }
  383. void handleCommandMessage (int commandId) override
  384. {
  385. Component::handleCommandMessage (commandId);
  386. if (commandId == PopupMenuSettings::dismissCommandId)
  387. dismissMenu (nullptr);
  388. }
  389. //==============================================================================
  390. void mouseMove (const MouseEvent& e) override { handleMouseEvent (e); }
  391. void mouseDown (const MouseEvent& e) override { handleMouseEvent (e); }
  392. void mouseDrag (const MouseEvent& e) override { handleMouseEvent (e); }
  393. void mouseUp (const MouseEvent& e) override { handleMouseEvent (e); }
  394. void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
  395. {
  396. alterChildYPos (roundToInt (-10.0f * wheel.deltaY * PopupMenuSettings::scrollZone));
  397. }
  398. void handleMouseEvent (const MouseEvent& e)
  399. {
  400. getMouseState (e.source).handleMouseEvent (e);
  401. }
  402. bool windowIsStillValid()
  403. {
  404. if (! isVisible())
  405. return false;
  406. if (componentAttachedTo != options.targetComponent)
  407. {
  408. dismissMenu (nullptr);
  409. return false;
  410. }
  411. if (MenuWindow* currentlyModalWindow = dynamic_cast<MenuWindow*> (Component::getCurrentlyModalComponent()))
  412. if (! treeContains (currentlyModalWindow))
  413. return false;
  414. return true;
  415. }
  416. static Array<MenuWindow*>& getActiveWindows()
  417. {
  418. static Array<MenuWindow*> activeMenuWindows;
  419. return activeMenuWindows;
  420. }
  421. MouseSourceState& getMouseState (MouseInputSource source)
  422. {
  423. for (int i = mouseSourceStates.size(); --i >= 0;)
  424. {
  425. MouseSourceState& ms = *mouseSourceStates.getUnchecked (i);
  426. if (ms.source == source)
  427. return ms;
  428. }
  429. MouseSourceState* ms = new MouseSourceState (*this, source);
  430. mouseSourceStates.add (ms);
  431. return *ms;
  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 (int i = 0; i < mouseSourceStates.size(); ++i)
  447. if (mouseSourceStates.getUnchecked (i)->isOver())
  448. return true;
  449. return false;
  450. }
  451. bool treeContains (const MenuWindow* const window) const noexcept
  452. {
  453. const MenuWindow* 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;
  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)
  486. {
  487. Rectangle<int> parentArea (Desktop::getInstance().getDisplays()
  488. .getDisplayContaining (targetPoint)
  489. #if JUCE_MAC
  490. .userArea);
  491. #else
  492. .totalArea); // on windows, don't stop the menu overlapping the taskbar
  493. #endif
  494. if (parentComponent == nullptr)
  495. return parentArea;
  496. return parentComponent->getLocalArea (nullptr,
  497. parentComponent->getScreenBounds()
  498. .reduced (PopupMenuSettings::borderSize)
  499. .getIntersection (parentArea));
  500. }
  501. void calculateWindowPos (Rectangle<int> target, const bool alignToRectangle)
  502. {
  503. const Rectangle<int> parentArea = getParentArea (target.getCentre());
  504. if (parentComponent != nullptr)
  505. target = parentComponent->getLocalArea (nullptr, target).getIntersection (parentArea);
  506. const int maxMenuHeight = parentArea.getHeight() - 24;
  507. int x, y, widthToUse, heightToUse;
  508. layoutMenuItems (parentArea.getWidth() - 24, maxMenuHeight, widthToUse, heightToUse);
  509. if (alignToRectangle)
  510. {
  511. x = target.getX();
  512. const int spaceUnder = parentArea.getHeight() - (target.getBottom() - parentArea.getY());
  513. const int spaceOver = target.getY() - parentArea.getY();
  514. if (heightToUse < spaceUnder - 30 || spaceUnder >= spaceOver)
  515. y = target.getBottom();
  516. else
  517. y = target.getY() - heightToUse;
  518. }
  519. else
  520. {
  521. bool tendTowardsRight = target.getCentreX() < parentArea.getCentreX();
  522. if (parent != nullptr)
  523. {
  524. if (parent->parent != nullptr)
  525. {
  526. const bool parentGoingRight = (parent->getX() + parent->getWidth() / 2
  527. > parent->parent->getX() + parent->parent->getWidth() / 2);
  528. if (parentGoingRight && target.getRight() + widthToUse < parentArea.getRight() - 4)
  529. tendTowardsRight = true;
  530. else if ((! parentGoingRight) && target.getX() > widthToUse + 4)
  531. tendTowardsRight = false;
  532. }
  533. else if (target.getRight() + widthToUse < parentArea.getRight() - 32)
  534. {
  535. tendTowardsRight = true;
  536. }
  537. }
  538. const int biggestSpace = jmax (parentArea.getRight() - target.getRight(),
  539. target.getX() - parentArea.getX()) - 32;
  540. if (biggestSpace < widthToUse)
  541. {
  542. layoutMenuItems (biggestSpace + target.getWidth() / 3, maxMenuHeight, widthToUse, heightToUse);
  543. if (numColumns > 1)
  544. layoutMenuItems (biggestSpace - 4, maxMenuHeight, widthToUse, heightToUse);
  545. tendTowardsRight = (parentArea.getRight() - target.getRight()) >= (target.getX() - parentArea.getX());
  546. }
  547. if (tendTowardsRight)
  548. x = jmin (parentArea.getRight() - widthToUse - 4, target.getRight());
  549. else
  550. x = jmax (parentArea.getX() + 4, target.getX() - widthToUse);
  551. y = target.getY();
  552. if (target.getCentreY() > parentArea.getCentreY())
  553. y = jmax (parentArea.getY(), target.getBottom() - heightToUse);
  554. }
  555. x = jmax (parentArea.getX() + 1, jmin (parentArea.getRight() - (widthToUse + 6), x));
  556. y = jmax (parentArea.getY() + 1, jmin (parentArea.getBottom() - (heightToUse + 6), y));
  557. windowPos.setBounds (x, y, widthToUse, heightToUse);
  558. // sets this flag if it's big enough to obscure any of its parent menus
  559. hideOnExit = parent != nullptr
  560. && parent->windowPos.intersects (windowPos.expanded (-4, -4));
  561. }
  562. void layoutMenuItems (const int maxMenuW, const int maxMenuH, int& width, int& height)
  563. {
  564. numColumns = 0;
  565. contentHeight = 0;
  566. int totalW;
  567. const int maximumNumColumns = options.maxColumns > 0 ? options.maxColumns : 7;
  568. do
  569. {
  570. ++numColumns;
  571. totalW = workOutBestSize (maxMenuW);
  572. if (totalW > maxMenuW)
  573. {
  574. numColumns = jmax (1, numColumns - 1);
  575. workOutBestSize (maxMenuW); // to update col widths
  576. break;
  577. }
  578. else if (totalW > maxMenuW / 2 || contentHeight < maxMenuH)
  579. {
  580. break;
  581. }
  582. } while (numColumns < maximumNumColumns);
  583. const int actualH = jmin (contentHeight, maxMenuH);
  584. needsToScroll = contentHeight > actualH;
  585. width = updateYPositions();
  586. height = actualH + PopupMenuSettings::borderSize * 2;
  587. }
  588. int workOutBestSize (const int maxMenuW)
  589. {
  590. int totalW = 0;
  591. contentHeight = 0;
  592. int childNum = 0;
  593. for (int col = 0; col < numColumns; ++col)
  594. {
  595. int colW = options.standardHeight, colH = 0;
  596. const int numChildren = jmin (items.size() - childNum,
  597. (items.size() + numColumns - 1) / numColumns);
  598. for (int i = numChildren; --i >= 0;)
  599. {
  600. colW = jmax (colW, items.getUnchecked (childNum + i)->getWidth());
  601. colH += items.getUnchecked (childNum + i)->getHeight();
  602. }
  603. colW = jmin (maxMenuW / jmax (1, numColumns - 2), colW + PopupMenuSettings::borderSize * 2);
  604. columnWidths.set (col, colW);
  605. totalW += colW;
  606. contentHeight = jmax (contentHeight, colH);
  607. childNum += numChildren;
  608. }
  609. // width must never be larger than the screen
  610. const int minWidth = jmin (maxMenuW, options.minWidth);
  611. if (totalW < minWidth)
  612. {
  613. totalW = minWidth;
  614. for (int col = 0; col < numColumns; ++col)
  615. columnWidths.set (0, totalW / numColumns);
  616. }
  617. return totalW;
  618. }
  619. void ensureItemIsVisible (const int itemID, int wantedY)
  620. {
  621. jassert (itemID != 0);
  622. for (int i = items.size(); --i >= 0;)
  623. {
  624. if (ItemComponent* const m = items.getUnchecked (i))
  625. {
  626. if (m->item.itemID == itemID
  627. && windowPos.getHeight() > PopupMenuSettings::scrollZone * 4)
  628. {
  629. const int currentY = m->getY();
  630. if (wantedY > 0 || currentY < 0 || m->getBottom() > windowPos.getHeight())
  631. {
  632. if (wantedY < 0)
  633. wantedY = jlimit (PopupMenuSettings::scrollZone,
  634. jmax (PopupMenuSettings::scrollZone,
  635. windowPos.getHeight() - (PopupMenuSettings::scrollZone + m->getHeight())),
  636. currentY);
  637. const Rectangle<int> parantArea = getParentArea (windowPos.getPosition());
  638. int deltaY = wantedY - currentY;
  639. windowPos.setSize (jmin (windowPos.getWidth(), parantArea.getWidth()),
  640. jmin (windowPos.getHeight(), parantArea.getHeight()));
  641. const int newY = jlimit (parantArea.getY(),
  642. parantArea.getBottom() - windowPos.getHeight(),
  643. windowPos.getY() + deltaY);
  644. deltaY -= newY - windowPos.getY();
  645. childYOffset -= deltaY;
  646. windowPos.setPosition (windowPos.getX(), newY);
  647. updateYPositions();
  648. }
  649. break;
  650. }
  651. }
  652. }
  653. }
  654. void resizeToBestWindowPos()
  655. {
  656. Rectangle<int> r (windowPos);
  657. if (childYOffset < 0)
  658. {
  659. r = r.withTop (r.getY() - childYOffset);
  660. }
  661. else if (childYOffset > 0)
  662. {
  663. const int spaceAtBottom = r.getHeight() - (contentHeight - childYOffset);
  664. if (spaceAtBottom > 0)
  665. r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom);
  666. }
  667. setBounds (r);
  668. updateYPositions();
  669. }
  670. void alterChildYPos (const int delta)
  671. {
  672. if (canScroll())
  673. {
  674. childYOffset += delta;
  675. if (delta < 0)
  676. childYOffset = jmax (childYOffset, 0);
  677. else if (delta > 0)
  678. childYOffset = jmin (childYOffset,
  679. contentHeight - windowPos.getHeight() + PopupMenuSettings::borderSize);
  680. updateYPositions();
  681. }
  682. else
  683. {
  684. childYOffset = 0;
  685. }
  686. resizeToBestWindowPos();
  687. repaint();
  688. }
  689. int updateYPositions()
  690. {
  691. int x = 0;
  692. int childNum = 0;
  693. for (int col = 0; col < numColumns; ++col)
  694. {
  695. const int numChildren = jmin (items.size() - childNum,
  696. (items.size() + numColumns - 1) / numColumns);
  697. const int colW = columnWidths [col];
  698. int y = PopupMenuSettings::borderSize - (childYOffset + (getY() - windowPos.getY()));
  699. for (int i = 0; i < numChildren; ++i)
  700. {
  701. Component* const c = items.getUnchecked (childNum + i);
  702. c->setBounds (x, y, colW, c->getHeight());
  703. y += c->getHeight();
  704. }
  705. x += colW;
  706. childNum += numChildren;
  707. }
  708. return x;
  709. }
  710. void setCurrentlyHighlightedChild (ItemComponent* const child)
  711. {
  712. if (currentChild != nullptr)
  713. currentChild->setHighlighted (false);
  714. currentChild = child;
  715. if (currentChild != nullptr)
  716. {
  717. currentChild->setHighlighted (true);
  718. timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter();
  719. }
  720. }
  721. bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); }
  722. bool showSubMenuFor (ItemComponent* const childComp)
  723. {
  724. activeSubMenu = nullptr;
  725. if (childComp != nullptr
  726. && hasActiveSubMenu (childComp->item))
  727. {
  728. activeSubMenu = new HelperClasses::MenuWindow (*(childComp->item.subMenu), this,
  729. options.withTargetScreenArea (childComp->getScreenBounds())
  730. .withMinimumWidth (0)
  731. .withTargetComponent (nullptr),
  732. false, dismissOnMouseUp, managerOfChosenCommand);
  733. activeSubMenu->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
  734. activeSubMenu->enterModalState (false);
  735. activeSubMenu->toFront (false);
  736. return true;
  737. }
  738. return false;
  739. }
  740. void triggerCurrentlyHighlightedItem()
  741. {
  742. if (currentChild != nullptr
  743. && canBeTriggered (currentChild->item)
  744. && (currentChild->item.customComponent == nullptr
  745. || currentChild->item.customComponent->isTriggeredAutomatically()))
  746. {
  747. dismissMenu (&currentChild->item);
  748. }
  749. }
  750. void selectNextItem (const int delta)
  751. {
  752. disableTimerUntilMouseMoves();
  753. int start = jmax (0, items.indexOf (currentChild));
  754. for (int i = items.size(); --i >= 0;)
  755. {
  756. start += delta;
  757. if (ItemComponent* mic = items.getUnchecked ((start + items.size()) % items.size()))
  758. {
  759. if (canBeTriggered (mic->item) || hasActiveSubMenu (mic->item))
  760. {
  761. setCurrentlyHighlightedChild (mic);
  762. break;
  763. }
  764. }
  765. }
  766. }
  767. void disableTimerUntilMouseMoves()
  768. {
  769. disableMouseMoves = true;
  770. if (parent != nullptr)
  771. parent->disableTimerUntilMouseMoves();
  772. }
  773. bool canScroll() const noexcept { return childYOffset != 0 || needsToScroll; }
  774. bool isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; }
  775. bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); }
  776. //==============================================================================
  777. MenuWindow* parent;
  778. const Options options;
  779. OwnedArray<ItemComponent> items;
  780. ApplicationCommandManager** managerOfChosenCommand;
  781. WeakReference<Component> componentAttachedTo;
  782. Component* parentComponent;
  783. Rectangle<int> windowPos;
  784. bool hasBeenOver, needsToScroll;
  785. bool dismissOnMouseUp, hideOnExit, disableMouseMoves, hasAnyJuceCompHadFocus;
  786. int numColumns, contentHeight, childYOffset;
  787. Component::SafePointer<ItemComponent> currentChild;
  788. ScopedPointer<MenuWindow> activeSubMenu;
  789. Array<int> columnWidths;
  790. uint32 windowCreationTime, lastFocusedTime, timeEnteredCurrentChildComp;
  791. OwnedArray<MouseSourceState> mouseSourceStates;
  792. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow)
  793. };
  794. //==============================================================================
  795. class MouseSourceState : private Timer
  796. {
  797. public:
  798. MouseSourceState (MenuWindow& w, MouseInputSource s)
  799. : window (w), source (s), scrollAcceleration (1.0),
  800. lastScrollTime (Time::getMillisecondCounter()),
  801. lastMouseMoveTime (0), isDown (false)
  802. {
  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;
  825. uint32 lastScrollTime, lastMouseMoveTime;
  826. bool isDown;
  827. void handleMousePosition (Point<int> globalMousePos)
  828. {
  829. const Point<int> localMousePos (window.getLocalPoint (nullptr, globalMousePos));
  830. const uint32 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. Component* c = window.getComponentAt (localMousePos);
  896. if (c == &window)
  897. c = nullptr;
  898. ItemComponent* 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. const Rectangle<int> itemScreenBounds (window.activeSubMenu->getScreenBounds());
  921. float subX = (float) itemScreenBounds.getX();
  922. Point<int> 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* const comp, const int w, const int h,
  971. const bool triggerMenuItemAutomaticallyWhenClicked)
  972. : PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked),
  973. width (w), height (h)
  974. {
  975. addAndMakeVisible (comp);
  976. }
  977. void getIdealSize (int& idealWidth, int& idealHeight) override
  978. {
  979. idealWidth = width;
  980. idealHeight = height;
  981. }
  982. void resized() override
  983. {
  984. if (Component* const child = getChildComponent (0))
  985. child->setBounds (getLocalBounds());
  986. }
  987. const int width, height;
  988. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper)
  989. };
  990. };
  991. //==============================================================================
  992. PopupMenu::PopupMenu()
  993. {
  994. }
  995. PopupMenu::PopupMenu (const PopupMenu& other)
  996. : lookAndFeel (other.lookAndFeel)
  997. {
  998. items.addCopiesOf (other.items);
  999. }
  1000. PopupMenu& PopupMenu::operator= (const PopupMenu& other)
  1001. {
  1002. if (this != &other)
  1003. {
  1004. lookAndFeel = other.lookAndFeel;
  1005. clear();
  1006. items.addCopiesOf (other.items);
  1007. }
  1008. return *this;
  1009. }
  1010. PopupMenu::PopupMenu (PopupMenu&& other) noexcept
  1011. : lookAndFeel (other.lookAndFeel)
  1012. {
  1013. items.swapWith (other.items);
  1014. }
  1015. PopupMenu& PopupMenu::operator= (PopupMenu&& other) noexcept
  1016. {
  1017. jassert (this != &other); // hopefully the compiler should make this situation impossible!
  1018. items.swapWith (other.items);
  1019. lookAndFeel = other.lookAndFeel;
  1020. return *this;
  1021. }
  1022. PopupMenu::~PopupMenu()
  1023. {
  1024. }
  1025. void PopupMenu::clear()
  1026. {
  1027. items.clear();
  1028. }
  1029. //==============================================================================
  1030. PopupMenu::Item::Item() noexcept
  1031. : itemID (0),
  1032. commandManager (nullptr),
  1033. colour (0x00000000),
  1034. isEnabled (true),
  1035. isTicked (false),
  1036. isSeparator (false),
  1037. isSectionHeader (false)
  1038. {
  1039. }
  1040. PopupMenu::Item::Item (const Item& other)
  1041. : text (other.text),
  1042. itemID (other.itemID),
  1043. subMenu (createCopyIfNotNull (other.subMenu.get())),
  1044. image (other.image != nullptr ? other.image->createCopy() : nullptr),
  1045. customComponent (other.customComponent),
  1046. customCallback (other.customCallback),
  1047. commandManager (other.commandManager),
  1048. shortcutKeyDescription (other.shortcutKeyDescription),
  1049. colour (other.colour),
  1050. isEnabled (other.isEnabled),
  1051. isTicked (other.isTicked),
  1052. isSeparator (other.isSeparator),
  1053. isSectionHeader (other.isSectionHeader)
  1054. {
  1055. }
  1056. PopupMenu::Item& PopupMenu::Item::operator= (const Item& other)
  1057. {
  1058. text = other.text;
  1059. itemID = other.itemID;
  1060. subMenu = createCopyIfNotNull (other.subMenu.get());
  1061. image = (other.image != nullptr ? other.image->createCopy() : nullptr);
  1062. customComponent = other.customComponent;
  1063. customCallback = other.customCallback;
  1064. commandManager = other.commandManager;
  1065. shortcutKeyDescription = other.shortcutKeyDescription;
  1066. colour = other.colour;
  1067. isEnabled = other.isEnabled;
  1068. isTicked = other.isTicked;
  1069. isSeparator = other.isSeparator;
  1070. isSectionHeader = other.isSectionHeader;
  1071. return *this;
  1072. }
  1073. void PopupMenu::addItem (const Item& newItem)
  1074. {
  1075. // An ID of 0 is used as a return value to indicate that the user
  1076. // didn't pick anything, so you shouldn't use it as the ID for an item..
  1077. jassert (newItem.itemID != 0
  1078. || newItem.isSeparator || newItem.isSectionHeader
  1079. || newItem.subMenu != nullptr);
  1080. items.add (new Item (newItem));
  1081. }
  1082. void PopupMenu::addItem (int itemResultID, const String& itemText, bool isActive, bool isTicked)
  1083. {
  1084. Item i;
  1085. i.text = itemText;
  1086. i.itemID = itemResultID;
  1087. i.isEnabled = isActive;
  1088. i.isTicked = isTicked;
  1089. addItem (i);
  1090. }
  1091. static Drawable* createDrawableFromImage (const Image& im)
  1092. {
  1093. if (im.isValid())
  1094. {
  1095. DrawableImage* d = new DrawableImage();
  1096. d->setImage (im);
  1097. return d;
  1098. }
  1099. return nullptr;
  1100. }
  1101. void PopupMenu::addItem (int itemResultID, const String& itemText, bool isActive, bool isTicked, const Image& iconToUse)
  1102. {
  1103. addItem (itemResultID, itemText, isActive, isTicked, createDrawableFromImage (iconToUse));
  1104. }
  1105. void PopupMenu::addItem (int itemResultID, const String& itemText, bool isActive, bool isTicked, Drawable* iconToUse)
  1106. {
  1107. Item i;
  1108. i.text = itemText;
  1109. i.itemID = itemResultID;
  1110. i.isEnabled = isActive;
  1111. i.isTicked = isTicked;
  1112. i.image = iconToUse;
  1113. addItem (i);
  1114. }
  1115. void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager,
  1116. const CommandID commandID,
  1117. const String& displayName,
  1118. Drawable* iconToUse)
  1119. {
  1120. jassert (commandManager != nullptr && commandID != 0);
  1121. if (const ApplicationCommandInfo* const registeredInfo = commandManager->getCommandForID (commandID))
  1122. {
  1123. ApplicationCommandInfo info (*registeredInfo);
  1124. ApplicationCommandTarget* const target = commandManager->getTargetForCommand (commandID, info);
  1125. Item i;
  1126. i.text = displayName.isNotEmpty() ? displayName : info.shortName;
  1127. i.itemID = (int) commandID;
  1128. i.commandManager = commandManager;
  1129. i.isEnabled = target != nullptr && (info.flags & ApplicationCommandInfo::isDisabled) == 0;
  1130. i.isTicked = (info.flags & ApplicationCommandInfo::isTicked) != 0;
  1131. i.image = iconToUse;
  1132. addItem (i);
  1133. }
  1134. }
  1135. void PopupMenu::addColouredItem (int itemResultID, const String& itemText, Colour itemTextColour,
  1136. bool isActive, bool isTicked, Drawable* iconToUse)
  1137. {
  1138. Item i;
  1139. i.text = itemText;
  1140. i.itemID = itemResultID;
  1141. i.colour = itemTextColour;
  1142. i.isEnabled = isActive;
  1143. i.isTicked = isTicked;
  1144. i.image = iconToUse;
  1145. addItem (i);
  1146. }
  1147. void PopupMenu::addColouredItem (int itemResultID, const String& itemText, Colour itemTextColour,
  1148. bool isActive, bool isTicked, const Image& iconToUse)
  1149. {
  1150. Item i;
  1151. i.text = itemText;
  1152. i.itemID = itemResultID;
  1153. i.colour = itemTextColour;
  1154. i.isEnabled = isActive;
  1155. i.isTicked = isTicked;
  1156. i.image = createDrawableFromImage (iconToUse);
  1157. addItem (i);
  1158. }
  1159. void PopupMenu::addCustomItem (int itemResultID, CustomComponent* cc, const PopupMenu* subMenu)
  1160. {
  1161. Item i;
  1162. i.itemID = itemResultID;
  1163. i.customComponent = cc;
  1164. i.subMenu = createCopyIfNotNull (subMenu);
  1165. addItem (i);
  1166. }
  1167. void PopupMenu::addCustomItem (int itemResultID, Component* customComponent, int idealWidth, int idealHeight,
  1168. bool triggerMenuItemAutomaticallyWhenClicked, const PopupMenu* subMenu)
  1169. {
  1170. addCustomItem (itemResultID,
  1171. new HelperClasses::NormalComponentWrapper (customComponent, idealWidth, idealHeight,
  1172. triggerMenuItemAutomaticallyWhenClicked),
  1173. subMenu);
  1174. }
  1175. void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, bool isActive)
  1176. {
  1177. addSubMenu (subMenuName, subMenu, isActive, nullptr, false, 0);
  1178. }
  1179. void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, bool isActive,
  1180. const Image& iconToUse, bool isTicked, int itemResultID)
  1181. {
  1182. addSubMenu (subMenuName, subMenu, isActive, createDrawableFromImage (iconToUse), isTicked, itemResultID);
  1183. }
  1184. void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, bool isActive,
  1185. Drawable* iconToUse, bool isTicked, int itemResultID)
  1186. {
  1187. Item i;
  1188. i.text = subMenuName;
  1189. i.itemID = itemResultID;
  1190. i.subMenu = new PopupMenu (subMenu);
  1191. i.isEnabled = isActive && (itemResultID != 0 || subMenu.getNumItems() > 0);
  1192. i.isTicked = isTicked;
  1193. i.image = iconToUse;
  1194. addItem (i);
  1195. }
  1196. void PopupMenu::addSeparator()
  1197. {
  1198. if (items.size() > 0 && ! items.getLast()->isSeparator)
  1199. {
  1200. Item i;
  1201. i.isSeparator = true;
  1202. addItem (i);
  1203. }
  1204. }
  1205. void PopupMenu::addSectionHeader (const String& title)
  1206. {
  1207. Item i;
  1208. i.text = title;
  1209. i.isSectionHeader = true;
  1210. addItem (i);
  1211. }
  1212. //==============================================================================
  1213. PopupMenu::Options::Options()
  1214. : targetComponent (nullptr),
  1215. parentComponent (nullptr),
  1216. visibleItemID (0),
  1217. minWidth (0),
  1218. maxColumns (0),
  1219. standardHeight (0)
  1220. {
  1221. targetArea.setPosition (Desktop::getMousePosition());
  1222. }
  1223. PopupMenu::Options PopupMenu::Options::withTargetComponent (Component* comp) const noexcept
  1224. {
  1225. Options o (*this);
  1226. o.targetComponent = comp;
  1227. if (comp != nullptr)
  1228. o.targetArea = comp->getScreenBounds();
  1229. return o;
  1230. }
  1231. PopupMenu::Options PopupMenu::Options::withTargetScreenArea (const Rectangle<int>& area) const noexcept
  1232. {
  1233. Options o (*this);
  1234. o.targetArea = area;
  1235. return o;
  1236. }
  1237. PopupMenu::Options PopupMenu::Options::withMinimumWidth (int w) const noexcept
  1238. {
  1239. Options o (*this);
  1240. o.minWidth = w;
  1241. return o;
  1242. }
  1243. PopupMenu::Options PopupMenu::Options::withMaximumNumColumns (int cols) const noexcept
  1244. {
  1245. Options o (*this);
  1246. o.maxColumns = cols;
  1247. return o;
  1248. }
  1249. PopupMenu::Options PopupMenu::Options::withStandardItemHeight (int height) const noexcept
  1250. {
  1251. Options o (*this);
  1252. o.standardHeight = height;
  1253. return o;
  1254. }
  1255. PopupMenu::Options PopupMenu::Options::withItemThatMustBeVisible (int idOfItemToBeVisible) const noexcept
  1256. {
  1257. Options o (*this);
  1258. o.visibleItemID = idOfItemToBeVisible;
  1259. return o;
  1260. }
  1261. PopupMenu::Options PopupMenu::Options::withParentComponent (Component* parent) const noexcept
  1262. {
  1263. Options o (*this);
  1264. o.parentComponent = parent;
  1265. return o;
  1266. }
  1267. Component* PopupMenu::createWindow (const Options& options,
  1268. ApplicationCommandManager** managerOfChosenCommand) const
  1269. {
  1270. if (items.size() > 0)
  1271. return new HelperClasses::MenuWindow (*this, nullptr, options,
  1272. ! options.targetArea.isEmpty(),
  1273. ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown(),
  1274. managerOfChosenCommand);
  1275. return nullptr;
  1276. }
  1277. //==============================================================================
  1278. // This invokes any command manager commands and deletes the menu window when it is dismissed
  1279. struct PopupMenuCompletionCallback : public ModalComponentManager::Callback
  1280. {
  1281. PopupMenuCompletionCallback()
  1282. : managerOfChosenCommand (nullptr),
  1283. prevFocused (Component::getCurrentlyFocusedComponent()),
  1284. prevTopLevel (prevFocused != nullptr ? prevFocused->getTopLevelComponent() : nullptr)
  1285. {
  1286. PopupMenuSettings::menuWasHiddenBecauseOfAppChange = false;
  1287. }
  1288. void modalStateFinished (int result) override
  1289. {
  1290. if (managerOfChosenCommand != nullptr && result != 0)
  1291. {
  1292. ApplicationCommandTarget::InvocationInfo info (result);
  1293. info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
  1294. managerOfChosenCommand->invoke (info, true);
  1295. }
  1296. // (this would be the place to fade out the component, if that's what's required)
  1297. component = nullptr;
  1298. if (! PopupMenuSettings::menuWasHiddenBecauseOfAppChange)
  1299. {
  1300. if (prevTopLevel != nullptr)
  1301. prevTopLevel->toFront (true);
  1302. if (prevFocused != nullptr)
  1303. prevFocused->grabKeyboardFocus();
  1304. }
  1305. }
  1306. ApplicationCommandManager* managerOfChosenCommand;
  1307. ScopedPointer<Component> component;
  1308. WeakReference<Component> prevFocused, prevTopLevel;
  1309. JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback)
  1310. };
  1311. int PopupMenu::showWithOptionalCallback (const Options& options, ModalComponentManager::Callback* const userCallback,
  1312. const bool canBeModal)
  1313. {
  1314. ScopedPointer<ModalComponentManager::Callback> userCallbackDeleter (userCallback);
  1315. ScopedPointer<PopupMenuCompletionCallback> callback (new PopupMenuCompletionCallback());
  1316. if (Component* window = createWindow (options, &(callback->managerOfChosenCommand)))
  1317. {
  1318. callback->component = window;
  1319. window->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
  1320. window->enterModalState (false, userCallbackDeleter.release());
  1321. ModalComponentManager::getInstance()->attachCallback (window, callback.release());
  1322. window->toFront (false); // need to do this after making it modal, or it could
  1323. // be stuck behind other comps that are already modal..
  1324. #if JUCE_MODAL_LOOPS_PERMITTED
  1325. if (userCallback == nullptr && canBeModal)
  1326. return window->runModalLoop();
  1327. #else
  1328. ignoreUnused (canBeModal);
  1329. jassert (! (userCallback == nullptr && canBeModal));
  1330. #endif
  1331. }
  1332. return 0;
  1333. }
  1334. //==============================================================================
  1335. #if JUCE_MODAL_LOOPS_PERMITTED
  1336. int PopupMenu::showMenu (const Options& options)
  1337. {
  1338. return showWithOptionalCallback (options, nullptr, true);
  1339. }
  1340. #endif
  1341. void PopupMenu::showMenuAsync (const Options& options, ModalComponentManager::Callback* userCallback)
  1342. {
  1343. #if ! JUCE_MODAL_LOOPS_PERMITTED
  1344. jassert (userCallback != nullptr);
  1345. #endif
  1346. showWithOptionalCallback (options, userCallback, false);
  1347. }
  1348. //==============================================================================
  1349. #if JUCE_MODAL_LOOPS_PERMITTED
  1350. int PopupMenu::show (const int itemIDThatMustBeVisible,
  1351. const int minimumWidth, const int maximumNumColumns,
  1352. const int standardItemHeight,
  1353. ModalComponentManager::Callback* callback)
  1354. {
  1355. return showWithOptionalCallback (Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
  1356. .withMinimumWidth (minimumWidth)
  1357. .withMaximumNumColumns (maximumNumColumns)
  1358. .withStandardItemHeight (standardItemHeight),
  1359. callback, true);
  1360. }
  1361. int PopupMenu::showAt (const Rectangle<int>& screenAreaToAttachTo,
  1362. const int itemIDThatMustBeVisible,
  1363. const int minimumWidth, const int maximumNumColumns,
  1364. const int standardItemHeight,
  1365. ModalComponentManager::Callback* callback)
  1366. {
  1367. return showWithOptionalCallback (Options().withTargetScreenArea (screenAreaToAttachTo)
  1368. .withItemThatMustBeVisible (itemIDThatMustBeVisible)
  1369. .withMinimumWidth (minimumWidth)
  1370. .withMaximumNumColumns (maximumNumColumns)
  1371. .withStandardItemHeight (standardItemHeight),
  1372. callback, true);
  1373. }
  1374. int PopupMenu::showAt (Component* componentToAttachTo,
  1375. const int itemIDThatMustBeVisible,
  1376. const int minimumWidth, const int maximumNumColumns,
  1377. const int standardItemHeight,
  1378. ModalComponentManager::Callback* callback)
  1379. {
  1380. Options options (Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
  1381. .withMinimumWidth (minimumWidth)
  1382. .withMaximumNumColumns (maximumNumColumns)
  1383. .withStandardItemHeight (standardItemHeight));
  1384. if (componentToAttachTo != nullptr)
  1385. options = options.withTargetComponent (componentToAttachTo);
  1386. return showWithOptionalCallback (options, callback, true);
  1387. }
  1388. #endif
  1389. bool JUCE_CALLTYPE PopupMenu::dismissAllActiveMenus()
  1390. {
  1391. const Array<HelperClasses::MenuWindow*>& windows = HelperClasses::MenuWindow::getActiveWindows();
  1392. const int numWindows = windows.size();
  1393. for (int i = numWindows; --i >= 0;)
  1394. if (HelperClasses::MenuWindow* const pmw = windows[i])
  1395. pmw->dismissMenu (nullptr);
  1396. return numWindows > 0;
  1397. }
  1398. //==============================================================================
  1399. int PopupMenu::getNumItems() const noexcept
  1400. {
  1401. int num = 0;
  1402. for (int i = items.size(); --i >= 0;)
  1403. if (! items.getUnchecked (i)->isSeparator)
  1404. ++num;
  1405. return num;
  1406. }
  1407. bool PopupMenu::containsCommandItem (const int commandID) const
  1408. {
  1409. for (int i = items.size(); --i >= 0;)
  1410. {
  1411. const Item& mi = *items.getUnchecked (i);
  1412. if ((mi.itemID == commandID && mi.commandManager != nullptr)
  1413. || (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID)))
  1414. return true;
  1415. }
  1416. return false;
  1417. }
  1418. bool PopupMenu::containsAnyActiveItems() const noexcept
  1419. {
  1420. for (int i = items.size(); --i >= 0;)
  1421. {
  1422. const Item& mi = *items.getUnchecked (i);
  1423. if (mi.subMenu != nullptr)
  1424. {
  1425. if (mi.subMenu->containsAnyActiveItems())
  1426. return true;
  1427. }
  1428. else if (mi.isEnabled)
  1429. {
  1430. return true;
  1431. }
  1432. }
  1433. return false;
  1434. }
  1435. void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel)
  1436. {
  1437. lookAndFeel = newLookAndFeel;
  1438. }
  1439. //==============================================================================
  1440. PopupMenu::CustomComponent::CustomComponent (bool autoTrigger)
  1441. : isHighlighted (false),
  1442. triggeredAutomatically (autoTrigger)
  1443. {
  1444. }
  1445. PopupMenu::CustomComponent::~CustomComponent()
  1446. {
  1447. }
  1448. void PopupMenu::CustomComponent::setHighlighted (bool shouldBeHighlighted)
  1449. {
  1450. isHighlighted = shouldBeHighlighted;
  1451. repaint();
  1452. }
  1453. void PopupMenu::CustomComponent::triggerMenuItem()
  1454. {
  1455. if (HelperClasses::ItemComponent* const mic = findParentComponentOfClass<HelperClasses::ItemComponent>())
  1456. {
  1457. if (HelperClasses::MenuWindow* const pmw = mic->findParentComponentOfClass<HelperClasses::MenuWindow>())
  1458. {
  1459. pmw->dismissMenu (&mic->item);
  1460. }
  1461. else
  1462. {
  1463. // something must have gone wrong with the component hierarchy if this happens..
  1464. jassertfalse;
  1465. }
  1466. }
  1467. else
  1468. {
  1469. // why isn't this component inside a menu? Not much point triggering the item if
  1470. // there's no menu.
  1471. jassertfalse;
  1472. }
  1473. }
  1474. //==============================================================================
  1475. PopupMenu::CustomCallback::CustomCallback() {}
  1476. PopupMenu::CustomCallback::~CustomCallback() {}
  1477. //==============================================================================
  1478. PopupMenu::MenuItemIterator::MenuItemIterator (const PopupMenu& m, bool searchR) : searchRecursively (searchR)
  1479. {
  1480. currentItem = nullptr;
  1481. index.add (0);
  1482. menus.add (&m);
  1483. }
  1484. PopupMenu::MenuItemIterator::~MenuItemIterator() {}
  1485. bool PopupMenu::MenuItemIterator::next()
  1486. {
  1487. if (index.size() == 0 || menus.getLast()->items.size() == 0)
  1488. return false;
  1489. currentItem = menus.getLast()->items.getUnchecked (index.getLast());
  1490. if (searchRecursively && currentItem->subMenu != nullptr)
  1491. {
  1492. index.add (0);
  1493. menus.add (currentItem->subMenu);
  1494. }
  1495. else
  1496. index.setUnchecked (index.size() - 1, index.getLast() + 1);
  1497. while (index.size() > 0 && index.getLast() >= menus.getLast()->items.size())
  1498. {
  1499. index.removeLast();
  1500. menus.removeLast();
  1501. if (index.size() > 0)
  1502. index.setUnchecked (index.size() - 1, index.getLast() + 1);
  1503. }
  1504. return true;
  1505. }
  1506. PopupMenu::Item& PopupMenu::MenuItemIterator::getItem() const noexcept
  1507. {
  1508. jassert (currentItem != nullptr);
  1509. return *(currentItem);
  1510. }