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.

1692 lines
58KB

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