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.

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