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.

1683 lines
57KB

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