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.

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