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.

1691 lines
58KB

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