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.

1700 lines
57KB

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