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.

1698 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()
  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().removeFirstMatchingValue (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&, const MouseWheelDetails& wheel)
  312. {
  313. alterChildYPos (roundToInt (-10.0f * wheel.deltaY * 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 (nullptr, 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 (nullptr, 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().getDisplays()
  519. .getDisplayContaining (target.getCentre())
  520. #if JUCE_MAC
  521. .userArea);
  522. #else
  523. .totalArea); // 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().getDisplays()
  655. .getDisplayContaining (windowPos.getPosition()).userArea);
  656. int deltaY = wantedY - currentY;
  657. windowPos.setSize (jmin (windowPos.getWidth(), mon.getWidth()),
  658. jmin (windowPos.getHeight(), mon.getHeight()));
  659. const int newY = jlimit (mon.getY(),
  660. mon.getBottom() - windowPos.getHeight(),
  661. windowPos.getY() + deltaY);
  662. deltaY -= newY - windowPos.getY();
  663. childYOffset -= deltaY;
  664. windowPos.setPosition (windowPos.getX(), newY);
  665. updateYPositions();
  666. }
  667. break;
  668. }
  669. }
  670. }
  671. void resizeToBestWindowPos()
  672. {
  673. Rectangle<int> r (windowPos);
  674. if (childYOffset < 0)
  675. {
  676. r = r.withTop (r.getY() - childYOffset);
  677. }
  678. else if (childYOffset > 0)
  679. {
  680. const int spaceAtBottom = r.getHeight() - (contentHeight - childYOffset);
  681. if (spaceAtBottom > 0)
  682. r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom);
  683. }
  684. setBounds (r);
  685. updateYPositions();
  686. }
  687. void alterChildYPos (const int delta)
  688. {
  689. if (canScroll())
  690. {
  691. childYOffset += delta;
  692. if (delta < 0)
  693. {
  694. childYOffset = jmax (childYOffset, 0);
  695. }
  696. else if (delta > 0)
  697. {
  698. childYOffset = jmin (childYOffset,
  699. contentHeight - windowPos.getHeight() + PopupMenuSettings::borderSize);
  700. }
  701. updateYPositions();
  702. }
  703. else
  704. {
  705. childYOffset = 0;
  706. }
  707. resizeToBestWindowPos();
  708. repaint();
  709. }
  710. int updateYPositions()
  711. {
  712. int x = 0;
  713. int childNum = 0;
  714. for (int col = 0; col < numColumns; ++col)
  715. {
  716. const int numChildren = jmin (items.size() - childNum,
  717. (items.size() + numColumns - 1) / numColumns);
  718. const int colW = columnWidths [col];
  719. int y = PopupMenuSettings::borderSize - (childYOffset + (getY() - windowPos.getY()));
  720. for (int i = 0; i < numChildren; ++i)
  721. {
  722. Component* const c = items.getUnchecked (childNum + i);
  723. c->setBounds (x, y, colW, c->getHeight());
  724. y += c->getHeight();
  725. }
  726. x += colW;
  727. childNum += numChildren;
  728. }
  729. return x;
  730. }
  731. void setCurrentlyHighlightedChild (PopupMenu::ItemComponent* const child)
  732. {
  733. if (currentChild != nullptr)
  734. currentChild->setHighlighted (false);
  735. currentChild = child;
  736. if (currentChild != nullptr)
  737. {
  738. currentChild->setHighlighted (true);
  739. timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter();
  740. }
  741. }
  742. bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); }
  743. bool showSubMenuFor (PopupMenu::ItemComponent* const childComp)
  744. {
  745. activeSubMenu = nullptr;
  746. if (childComp != nullptr
  747. && childComp->itemInfo.hasActiveSubMenu())
  748. {
  749. activeSubMenu = new Window (*(childComp->itemInfo.subMenu), this,
  750. options.withTargetScreenArea (childComp->getScreenBounds())
  751. .withMinimumWidth (0)
  752. .withTargetComponent (nullptr),
  753. false, dismissOnMouseUp, managerOfChosenCommand);
  754. activeSubMenu->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
  755. activeSubMenu->enterModalState (false);
  756. activeSubMenu->toFront (false);
  757. return true;
  758. }
  759. return false;
  760. }
  761. void highlightItemUnderMouse (const Point<int>& globalMousePos, const Point<int>& localMousePos, const uint32 timeNow)
  762. {
  763. if (globalMousePos != lastMousePos || timeNow > lastMouseMoveTime + 350)
  764. {
  765. isOver = reallyContains (localMousePos, true);
  766. if (isOver)
  767. hasBeenOver = true;
  768. if (lastMousePos.getDistanceFrom (globalMousePos) > 2)
  769. {
  770. lastMouseMoveTime = timeNow;
  771. if (disableMouseMoves && isOver)
  772. disableMouseMoves = false;
  773. }
  774. if (disableMouseMoves || (activeSubMenu != nullptr && activeSubMenu->isOverChildren()))
  775. return;
  776. bool isMovingTowardsMenu = false;
  777. if (isOver && (activeSubMenu != nullptr) && globalMousePos != lastMousePos)
  778. {
  779. // try to intelligently guess whether the user is moving the mouse towards a currently-open
  780. // submenu. To do this, look at whether the mouse stays inside a triangular region that
  781. // extends from the last mouse pos to the submenu's rectangle..
  782. float subX = (float) activeSubMenu->getScreenX();
  783. if (activeSubMenu->getX() > getX())
  784. {
  785. lastMousePos -= Point<int> (2, 0); // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels
  786. }
  787. else
  788. {
  789. lastMousePos += Point<int> (2, 0);
  790. subX += activeSubMenu->getWidth();
  791. }
  792. Path areaTowardsSubMenu;
  793. areaTowardsSubMenu.addTriangle ((float) lastMousePos.x, (float) lastMousePos.y,
  794. subX, (float) activeSubMenu->getScreenY(),
  795. subX, (float) (activeSubMenu->getScreenY() + activeSubMenu->getHeight()));
  796. isMovingTowardsMenu = areaTowardsSubMenu.contains (globalMousePos.toFloat());
  797. }
  798. lastMousePos = globalMousePos;
  799. if (! isMovingTowardsMenu)
  800. {
  801. Component* c = getComponentAt (localMousePos);
  802. if (c == this)
  803. c = nullptr;
  804. PopupMenu::ItemComponent* itemUnderMouse = dynamic_cast <PopupMenu::ItemComponent*> (c);
  805. if (itemUnderMouse == nullptr && c != nullptr)
  806. itemUnderMouse = c->findParentComponentOfClass<PopupMenu::ItemComponent>();
  807. if (itemUnderMouse != currentChild
  808. && (isOver || (activeSubMenu == nullptr) || ! activeSubMenu->isVisible()))
  809. {
  810. if (isOver && (c != nullptr) && (activeSubMenu != nullptr))
  811. activeSubMenu->hide (nullptr, true);
  812. if (! isOver)
  813. itemUnderMouse = nullptr;
  814. setCurrentlyHighlightedChild (itemUnderMouse);
  815. }
  816. }
  817. }
  818. }
  819. void checkButtonState (const Point<int>& localMousePos, const uint32 timeNow,
  820. const bool wasDown, const bool overScrollArea, const bool isOverAny)
  821. {
  822. isDown = hasBeenOver
  823. && (ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown()
  824. || ModifierKeys::getCurrentModifiersRealtime().isAnyMouseButtonDown());
  825. if (! doesAnyJuceCompHaveFocus())
  826. {
  827. if (timeNow > lastFocusedTime + 10)
  828. {
  829. PopupMenuSettings::menuWasHiddenBecauseOfAppChange = true;
  830. dismissMenu (nullptr);
  831. // Note: this object may have been deleted by the previous call..
  832. }
  833. }
  834. else if (wasDown && timeNow > menuCreationTime + 250
  835. && ! (isDown || overScrollArea))
  836. {
  837. isOver = reallyContains (localMousePos, true);
  838. if (isOver)
  839. triggerCurrentlyHighlightedItem();
  840. else if ((hasBeenOver || ! dismissOnMouseUp) && ! isOverAny)
  841. dismissMenu (nullptr);
  842. // Note: this object may have been deleted by the previous call..
  843. }
  844. else
  845. {
  846. lastFocusedTime = timeNow;
  847. }
  848. }
  849. void triggerCurrentlyHighlightedItem()
  850. {
  851. if (currentChild != nullptr
  852. && currentChild->itemInfo.canBeTriggered()
  853. && (currentChild->itemInfo.customComp == nullptr
  854. || currentChild->itemInfo.customComp->isTriggeredAutomatically()))
  855. {
  856. dismissMenu (&currentChild->itemInfo);
  857. }
  858. }
  859. void selectNextItem (const int delta)
  860. {
  861. disableTimerUntilMouseMoves();
  862. PopupMenu::ItemComponent* mic = nullptr;
  863. bool wasLastOne = (currentChild == nullptr);
  864. const int numItems = items.size();
  865. for (int i = 0; i < numItems + 1; ++i)
  866. {
  867. int index = (delta > 0) ? i : (numItems - 1 - i);
  868. index = (index + numItems) % numItems;
  869. mic = items.getUnchecked (index);
  870. if (mic != nullptr && (mic->itemInfo.canBeTriggered() || mic->itemInfo.hasActiveSubMenu())
  871. && wasLastOne)
  872. break;
  873. if (mic == currentChild)
  874. wasLastOne = true;
  875. }
  876. setCurrentlyHighlightedChild (mic);
  877. }
  878. void disableTimerUntilMouseMoves()
  879. {
  880. disableMouseMoves = true;
  881. if (owner != nullptr)
  882. owner->disableTimerUntilMouseMoves();
  883. }
  884. bool canScroll() const noexcept { return childYOffset != 0 || needsToScroll; }
  885. bool isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; }
  886. bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); }
  887. bool scrollIfNecessary (const Point<int>& localMousePos, const uint32 timeNow)
  888. {
  889. if (canScroll()
  890. && (isOver || (isDown && isPositiveAndBelow (localMousePos.x, getWidth()))))
  891. {
  892. if (isTopScrollZoneActive() && localMousePos.y < PopupMenuSettings::scrollZone)
  893. return scroll (timeNow, -1);
  894. if (isBottomScrollZoneActive() && localMousePos.y > getHeight() - PopupMenuSettings::scrollZone)
  895. return scroll (timeNow, 1);
  896. }
  897. scrollAcceleration = 1.0;
  898. return false;
  899. }
  900. bool scroll (const uint32 timeNow, const int direction)
  901. {
  902. if (timeNow > lastScrollTime + 20)
  903. {
  904. scrollAcceleration = jmin (4.0, scrollAcceleration * 1.04);
  905. int amount = 0;
  906. for (int i = 0; i < items.size() && amount == 0; ++i)
  907. amount = ((int) scrollAcceleration) * items.getUnchecked(i)->getHeight();
  908. alterChildYPos (amount * direction);
  909. lastScrollTime = timeNow;
  910. }
  911. lastMousePos = Point<int> (-1, -1); // to trigger a mouse-move
  912. return true;
  913. }
  914. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Window);
  915. };
  916. //==============================================================================
  917. PopupMenu::PopupMenu()
  918. : lookAndFeel (nullptr)
  919. {
  920. }
  921. PopupMenu::PopupMenu (const PopupMenu& other)
  922. : lookAndFeel (other.lookAndFeel)
  923. {
  924. items.addCopiesOf (other.items);
  925. }
  926. PopupMenu& PopupMenu::operator= (const PopupMenu& other)
  927. {
  928. if (this != &other)
  929. {
  930. lookAndFeel = other.lookAndFeel;
  931. clear();
  932. items.addCopiesOf (other.items);
  933. }
  934. return *this;
  935. }
  936. #if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
  937. PopupMenu::PopupMenu (PopupMenu&& other) noexcept
  938. : lookAndFeel (other.lookAndFeel)
  939. {
  940. items.swapWithArray (other.items);
  941. }
  942. PopupMenu& PopupMenu::operator= (PopupMenu&& other) noexcept
  943. {
  944. jassert (this != &other); // hopefully the compiler should make this situation impossible!
  945. items.swapWithArray (other.items);
  946. lookAndFeel = other.lookAndFeel;
  947. return *this;
  948. }
  949. #endif
  950. PopupMenu::~PopupMenu()
  951. {
  952. }
  953. void PopupMenu::clear()
  954. {
  955. items.clear();
  956. }
  957. void PopupMenu::addItem (const int itemResultId, const String& itemText,
  958. const bool isActive, const bool isTicked, const Image& iconToUse)
  959. {
  960. jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user
  961. // didn't pick anything, so you shouldn't use it as the id
  962. // for an item..
  963. items.add (new Item (itemResultId, itemText, isActive, isTicked, iconToUse,
  964. Colours::black, false, nullptr, nullptr, nullptr));
  965. }
  966. void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager,
  967. const int commandID,
  968. const String& displayName)
  969. {
  970. jassert (commandManager != nullptr && commandID != 0);
  971. const ApplicationCommandInfo* const registeredInfo = commandManager->getCommandForID (commandID);
  972. if (registeredInfo != nullptr)
  973. {
  974. ApplicationCommandInfo info (*registeredInfo);
  975. ApplicationCommandTarget* const target = commandManager->getTargetForCommand (commandID, info);
  976. items.add (new Item (commandID,
  977. displayName.isNotEmpty() ? displayName
  978. : info.shortName,
  979. target != nullptr && (info.flags & ApplicationCommandInfo::isDisabled) == 0,
  980. (info.flags & ApplicationCommandInfo::isTicked) != 0,
  981. Image::null,
  982. Colours::black,
  983. false,
  984. nullptr, nullptr,
  985. commandManager));
  986. }
  987. }
  988. void PopupMenu::addColouredItem (const int itemResultId,
  989. const String& itemText,
  990. const Colour& itemTextColour,
  991. const bool isActive,
  992. const bool isTicked,
  993. const Image& iconToUse)
  994. {
  995. jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user
  996. // didn't pick anything, so you shouldn't use it as the id
  997. // for an item..
  998. items.add (new Item (itemResultId, itemText, isActive, isTicked, iconToUse,
  999. itemTextColour, true, nullptr, nullptr, nullptr));
  1000. }
  1001. //==============================================================================
  1002. void PopupMenu::addCustomItem (const int itemResultId, CustomComponent* const customComponent)
  1003. {
  1004. jassert (itemResultId != 0); // 0 is used as a return value to indicate that the user
  1005. // didn't pick anything, so you shouldn't use it as the id
  1006. // for an item..
  1007. items.add (new Item (itemResultId, String::empty, true, false, Image::null,
  1008. Colours::black, false, customComponent, nullptr, nullptr));
  1009. }
  1010. class PopupMenu::NormalComponentWrapper : public PopupMenu::CustomComponent
  1011. {
  1012. public:
  1013. NormalComponentWrapper (Component* const comp, const int w, const int h,
  1014. const bool triggerMenuItemAutomaticallyWhenClicked)
  1015. : PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked),
  1016. width (w), height (h)
  1017. {
  1018. addAndMakeVisible (comp);
  1019. }
  1020. void getIdealSize (int& idealWidth, int& idealHeight)
  1021. {
  1022. idealWidth = width;
  1023. idealHeight = height;
  1024. }
  1025. void resized()
  1026. {
  1027. if (getChildComponent(0) != nullptr)
  1028. getChildComponent(0)->setBounds (getLocalBounds());
  1029. }
  1030. private:
  1031. const int width, height;
  1032. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper);
  1033. };
  1034. void PopupMenu::addCustomItem (const int itemResultId,
  1035. Component* customComponent,
  1036. int idealWidth, int idealHeight,
  1037. const bool triggerMenuItemAutomaticallyWhenClicked)
  1038. {
  1039. addCustomItem (itemResultId,
  1040. new NormalComponentWrapper (customComponent, idealWidth, idealHeight,
  1041. triggerMenuItemAutomaticallyWhenClicked));
  1042. }
  1043. //==============================================================================
  1044. void PopupMenu::addSubMenu (const String& subMenuName,
  1045. const PopupMenu& subMenu,
  1046. const bool isActive,
  1047. const Image& iconToUse,
  1048. const bool isTicked)
  1049. {
  1050. items.add (new Item (0, subMenuName, isActive && (subMenu.getNumItems() > 0), isTicked,
  1051. iconToUse, Colours::black, false, nullptr, &subMenu, nullptr));
  1052. }
  1053. void PopupMenu::addSeparator()
  1054. {
  1055. if (items.size() > 0 && ! items.getLast()->isSeparator)
  1056. items.add (new Item());
  1057. }
  1058. //==============================================================================
  1059. class PopupMenu::HeaderItemComponent : public PopupMenu::CustomComponent
  1060. {
  1061. public:
  1062. HeaderItemComponent (const String& name)
  1063. : PopupMenu::CustomComponent (false)
  1064. {
  1065. setName (name);
  1066. }
  1067. void paint (Graphics& g)
  1068. {
  1069. g.setFont (getLookAndFeel().getPopupMenuFont().boldened());
  1070. g.setColour (findColour (PopupMenu::headerTextColourId));
  1071. g.drawFittedText (getName(),
  1072. 12, 0, getWidth() - 16, proportionOfHeight (0.8f),
  1073. Justification::bottomLeft, 1);
  1074. }
  1075. void getIdealSize (int& idealWidth, int& idealHeight)
  1076. {
  1077. getLookAndFeel().getIdealPopupMenuItemSize (getName(), false, -1, idealWidth, idealHeight);
  1078. idealHeight += idealHeight / 2;
  1079. idealWidth += idealWidth / 4;
  1080. }
  1081. private:
  1082. JUCE_LEAK_DETECTOR (HeaderItemComponent);
  1083. };
  1084. void PopupMenu::addSectionHeader (const String& title)
  1085. {
  1086. addCustomItem (0X4734a34f, new HeaderItemComponent (title));
  1087. }
  1088. //==============================================================================
  1089. PopupMenu::Options::Options()
  1090. : targetComponent (nullptr),
  1091. visibleItemID (0),
  1092. minWidth (0),
  1093. maxColumns (0),
  1094. standardHeight (0)
  1095. {
  1096. targetArea.setPosition (Desktop::getMousePosition());
  1097. }
  1098. PopupMenu::Options PopupMenu::Options::withTargetComponent (Component* comp) const noexcept
  1099. {
  1100. Options o (*this);
  1101. o.targetComponent = comp;
  1102. if (comp != nullptr)
  1103. o.targetArea = comp->getScreenBounds();
  1104. return o;
  1105. }
  1106. PopupMenu::Options PopupMenu::Options::withTargetScreenArea (const Rectangle<int>& area) const noexcept
  1107. {
  1108. Options o (*this);
  1109. o.targetArea = area;
  1110. return o;
  1111. }
  1112. PopupMenu::Options PopupMenu::Options::withMinimumWidth (int w) const noexcept
  1113. {
  1114. Options o (*this);
  1115. o.minWidth = w;
  1116. return o;
  1117. }
  1118. PopupMenu::Options PopupMenu::Options::withMaximumNumColumns (int cols) const noexcept
  1119. {
  1120. Options o (*this);
  1121. o.maxColumns = cols;
  1122. return o;
  1123. }
  1124. PopupMenu::Options PopupMenu::Options::withStandardItemHeight (int height) const noexcept
  1125. {
  1126. Options o (*this);
  1127. o.standardHeight = height;
  1128. return o;
  1129. }
  1130. PopupMenu::Options PopupMenu::Options::withItemThatMustBeVisible (int idOfItemToBeVisible) const noexcept
  1131. {
  1132. Options o (*this);
  1133. o.visibleItemID = idOfItemToBeVisible;
  1134. return o;
  1135. }
  1136. Component* PopupMenu::createWindow (const Options& options,
  1137. ApplicationCommandManager** managerOfChosenCommand) const
  1138. {
  1139. if (items.size() > 0)
  1140. return new Window (*this, nullptr, options,
  1141. ! options.targetArea.isEmpty(),
  1142. ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown(),
  1143. managerOfChosenCommand);
  1144. return nullptr;
  1145. }
  1146. //==============================================================================
  1147. // This invokes any command manager commands and deletes the menu window when it is dismissed
  1148. class PopupMenuCompletionCallback : public ModalComponentManager::Callback
  1149. {
  1150. public:
  1151. PopupMenuCompletionCallback()
  1152. : managerOfChosenCommand (nullptr),
  1153. prevFocused (Component::getCurrentlyFocusedComponent()),
  1154. prevTopLevel (prevFocused != nullptr ? prevFocused->getTopLevelComponent() : nullptr)
  1155. {
  1156. PopupMenuSettings::menuWasHiddenBecauseOfAppChange = false;
  1157. }
  1158. void modalStateFinished (int result)
  1159. {
  1160. if (managerOfChosenCommand != nullptr && result != 0)
  1161. {
  1162. ApplicationCommandTarget::InvocationInfo info (result);
  1163. info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
  1164. managerOfChosenCommand->invoke (info, true);
  1165. }
  1166. // (this would be the place to fade out the component, if that's what's required)
  1167. component = nullptr;
  1168. if (! PopupMenuSettings::menuWasHiddenBecauseOfAppChange)
  1169. {
  1170. if (prevTopLevel != nullptr)
  1171. prevTopLevel->toFront (true);
  1172. if (prevFocused != nullptr)
  1173. prevFocused->grabKeyboardFocus();
  1174. }
  1175. }
  1176. ApplicationCommandManager* managerOfChosenCommand;
  1177. ScopedPointer<Component> component;
  1178. WeakReference<Component> prevFocused, prevTopLevel;
  1179. private:
  1180. JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback);
  1181. };
  1182. int PopupMenu::showWithOptionalCallback (const Options& options, ModalComponentManager::Callback* const userCallback,
  1183. const bool canBeModal)
  1184. {
  1185. ScopedPointer<ModalComponentManager::Callback> userCallbackDeleter (userCallback);
  1186. ScopedPointer<PopupMenuCompletionCallback> callback (new PopupMenuCompletionCallback());
  1187. Component* window = createWindow (options, &(callback->managerOfChosenCommand));
  1188. if (window == nullptr)
  1189. return 0;
  1190. callback->component = window;
  1191. window->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
  1192. window->enterModalState (false, userCallbackDeleter.release());
  1193. ModalComponentManager::getInstance()->attachCallback (window, callback.release());
  1194. window->toFront (false); // need to do this after making it modal, or it could
  1195. // be stuck behind other comps that are already modal..
  1196. #if JUCE_MODAL_LOOPS_PERMITTED
  1197. return (userCallback == nullptr && canBeModal) ? window->runModalLoop() : 0;
  1198. #else
  1199. jassert (! (userCallback == nullptr && canBeModal));
  1200. return 0;
  1201. #endif
  1202. }
  1203. //==============================================================================
  1204. #if JUCE_MODAL_LOOPS_PERMITTED
  1205. int PopupMenu::showMenu (const Options& options)
  1206. {
  1207. return showWithOptionalCallback (options, nullptr, true);
  1208. }
  1209. #endif
  1210. void PopupMenu::showMenuAsync (const Options& options, ModalComponentManager::Callback* userCallback)
  1211. {
  1212. #if ! JUCE_MODAL_LOOPS_PERMITTED
  1213. jassert (userCallback != nullptr);
  1214. #endif
  1215. showWithOptionalCallback (options, userCallback, false);
  1216. }
  1217. //==============================================================================
  1218. #if JUCE_MODAL_LOOPS_PERMITTED
  1219. int PopupMenu::show (const int itemIdThatMustBeVisible,
  1220. const int minimumWidth, const int maximumNumColumns,
  1221. const int standardItemHeight,
  1222. ModalComponentManager::Callback* callback)
  1223. {
  1224. return showWithOptionalCallback (Options().withItemThatMustBeVisible (itemIdThatMustBeVisible)
  1225. .withMinimumWidth (minimumWidth)
  1226. .withMaximumNumColumns (maximumNumColumns)
  1227. .withStandardItemHeight (standardItemHeight),
  1228. callback, true);
  1229. }
  1230. int PopupMenu::showAt (const Rectangle<int>& screenAreaToAttachTo,
  1231. const int itemIdThatMustBeVisible,
  1232. const int minimumWidth, const int maximumNumColumns,
  1233. const int standardItemHeight,
  1234. ModalComponentManager::Callback* callback)
  1235. {
  1236. return showWithOptionalCallback (Options().withTargetScreenArea (screenAreaToAttachTo)
  1237. .withItemThatMustBeVisible (itemIdThatMustBeVisible)
  1238. .withMinimumWidth (minimumWidth)
  1239. .withMaximumNumColumns (maximumNumColumns)
  1240. .withStandardItemHeight (standardItemHeight),
  1241. callback, true);
  1242. }
  1243. int PopupMenu::showAt (Component* componentToAttachTo,
  1244. const int itemIdThatMustBeVisible,
  1245. const int minimumWidth, const int maximumNumColumns,
  1246. const int standardItemHeight,
  1247. ModalComponentManager::Callback* callback)
  1248. {
  1249. Options options (Options().withItemThatMustBeVisible (itemIdThatMustBeVisible)
  1250. .withMinimumWidth (minimumWidth)
  1251. .withMaximumNumColumns (maximumNumColumns)
  1252. .withStandardItemHeight (standardItemHeight));
  1253. if (componentToAttachTo != nullptr)
  1254. options = options.withTargetComponent (componentToAttachTo);
  1255. return showWithOptionalCallback (options, callback, true);
  1256. }
  1257. #endif
  1258. bool JUCE_CALLTYPE PopupMenu::dismissAllActiveMenus()
  1259. {
  1260. Array<Window*>& windows = Window::getActiveWindows();
  1261. const int numWindows = windows.size();
  1262. for (int i = numWindows; --i >= 0;)
  1263. {
  1264. Window* const pmw = windows[i];
  1265. if (pmw != nullptr)
  1266. pmw->dismissMenu (nullptr);
  1267. }
  1268. return numWindows > 0;
  1269. }
  1270. //==============================================================================
  1271. int PopupMenu::getNumItems() const noexcept
  1272. {
  1273. int num = 0;
  1274. for (int i = items.size(); --i >= 0;)
  1275. if (! items.getUnchecked(i)->isSeparator)
  1276. ++num;
  1277. return num;
  1278. }
  1279. bool PopupMenu::containsCommandItem (const int commandID) const
  1280. {
  1281. for (int i = items.size(); --i >= 0;)
  1282. {
  1283. const Item* const mi = items.getUnchecked (i);
  1284. if ((mi->itemId == commandID && mi->commandManager != nullptr)
  1285. || (mi->subMenu != nullptr && mi->subMenu->containsCommandItem (commandID)))
  1286. {
  1287. return true;
  1288. }
  1289. }
  1290. return false;
  1291. }
  1292. bool PopupMenu::containsAnyActiveItems() const noexcept
  1293. {
  1294. for (int i = items.size(); --i >= 0;)
  1295. {
  1296. const Item* const mi = items.getUnchecked (i);
  1297. if (mi->subMenu != nullptr)
  1298. {
  1299. if (mi->subMenu->containsAnyActiveItems())
  1300. return true;
  1301. }
  1302. else if (mi->active)
  1303. {
  1304. return true;
  1305. }
  1306. }
  1307. return false;
  1308. }
  1309. void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel)
  1310. {
  1311. lookAndFeel = newLookAndFeel;
  1312. }
  1313. //==============================================================================
  1314. PopupMenu::CustomComponent::CustomComponent (const bool isTriggeredAutomatically_)
  1315. : isHighlighted (false),
  1316. triggeredAutomatically (isTriggeredAutomatically_)
  1317. {
  1318. }
  1319. PopupMenu::CustomComponent::~CustomComponent()
  1320. {
  1321. }
  1322. void PopupMenu::CustomComponent::setHighlighted (bool shouldBeHighlighted)
  1323. {
  1324. isHighlighted = shouldBeHighlighted;
  1325. repaint();
  1326. }
  1327. void PopupMenu::CustomComponent::triggerMenuItem()
  1328. {
  1329. PopupMenu::ItemComponent* const mic = dynamic_cast <PopupMenu::ItemComponent*> (getParentComponent());
  1330. if (mic != nullptr)
  1331. {
  1332. PopupMenu::Window* const pmw = dynamic_cast <PopupMenu::Window*> (mic->getParentComponent());
  1333. if (pmw != nullptr)
  1334. {
  1335. pmw->dismissMenu (&mic->itemInfo);
  1336. }
  1337. else
  1338. {
  1339. // something must have gone wrong with the component hierarchy if this happens..
  1340. jassertfalse;
  1341. }
  1342. }
  1343. else
  1344. {
  1345. // why isn't this component inside a menu? Not much point triggering the item if
  1346. // there's no menu.
  1347. jassertfalse;
  1348. }
  1349. }
  1350. //==============================================================================
  1351. PopupMenu::MenuItemIterator::MenuItemIterator (const PopupMenu& menu_)
  1352. : subMenu (nullptr),
  1353. itemId (0),
  1354. isSeparator (false),
  1355. isTicked (false),
  1356. isEnabled (false),
  1357. isCustomComponent (false),
  1358. isSectionHeader (false),
  1359. customColour (nullptr),
  1360. menu (menu_),
  1361. index (0)
  1362. {
  1363. }
  1364. PopupMenu::MenuItemIterator::~MenuItemIterator()
  1365. {
  1366. }
  1367. bool PopupMenu::MenuItemIterator::next()
  1368. {
  1369. if (index >= menu.items.size())
  1370. return false;
  1371. const Item* const item = menu.items.getUnchecked (index);
  1372. ++index;
  1373. if (item->isSeparator && index >= menu.items.size()) // (avoid showing a separator at the end)
  1374. return false;
  1375. itemName = item->customComp != nullptr ? item->customComp->getName() : item->text;
  1376. subMenu = item->subMenu;
  1377. itemId = item->itemId;
  1378. isSeparator = item->isSeparator;
  1379. isTicked = item->isTicked;
  1380. isEnabled = item->active;
  1381. isSectionHeader = dynamic_cast <HeaderItemComponent*> (static_cast <CustomComponent*> (item->customComp)) != nullptr;
  1382. isCustomComponent = (! isSectionHeader) && item->customComp != nullptr;
  1383. customColour = item->usesColour ? &(item->textColour) : nullptr;
  1384. customImage = item->image;
  1385. commandManager = item->commandManager;
  1386. return true;
  1387. }