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.

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