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.

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