1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 namespace PopupMenuSettings
30 {
31     const int scrollZone = 24;
32     const int dismissCommandId = 0x6287345f;
33 
34     static bool menuWasHiddenBecauseOfAppChange = false;
35 }
36 
37 //==============================================================================
38 struct PopupMenu::HelperClasses
39 {
40 
41 class MouseSourceState;
42 struct MenuWindow;
43 
canBeTriggeredjuce::PopupMenu::HelperClasses44 static bool canBeTriggered (const PopupMenu::Item& item) noexcept        { return item.isEnabled && item.itemID != 0 && ! item.isSectionHeader; }
hasActiveSubMenujuce::PopupMenu::HelperClasses45 static bool hasActiveSubMenu (const PopupMenu::Item& item) noexcept      { return item.isEnabled && item.subMenu != nullptr && item.subMenu->items.size() > 0; }
getColourjuce::PopupMenu::HelperClasses46 static const Colour* getColour (const PopupMenu::Item& item) noexcept    { return item.colour != Colour() ? &item.colour : nullptr; }
hasSubMenujuce::PopupMenu::HelperClasses47 static bool hasSubMenu (const PopupMenu::Item& item) noexcept            { return item.subMenu != nullptr && (item.itemID == 0 || item.subMenu->getNumItems() > 0); }
48 
49 //==============================================================================
50 struct HeaderItemComponent  : public PopupMenu::CustomComponent
51 {
HeaderItemComponentjuce::PopupMenu::HelperClasses::HeaderItemComponent52     HeaderItemComponent (const String& name)  : PopupMenu::CustomComponent (false)
53     {
54         setName (name);
55     }
56 
paintjuce::PopupMenu::HelperClasses::HeaderItemComponent57     void paint (Graphics& g) override
58     {
59         getLookAndFeel().drawPopupMenuSectionHeader (g, getLocalBounds(), getName());
60     }
61 
getIdealSizejuce::PopupMenu::HelperClasses::HeaderItemComponent62     void getIdealSize (int& idealWidth, int& idealHeight) override
63     {
64         getLookAndFeel().getIdealPopupMenuItemSize (getName(), false, -1, idealWidth, idealHeight);
65         idealHeight += idealHeight / 2;
66         idealWidth += idealWidth / 4;
67     }
68 
69     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HeaderItemComponent)
70 };
71 
72 //==============================================================================
73 struct ItemComponent  : public Component
74 {
ItemComponentjuce::PopupMenu::HelperClasses::ItemComponent75     ItemComponent (const PopupMenu::Item& i, int standardItemHeight, MenuWindow& parent)
76       : item (i), customComp (i.customComponent)
77     {
78         if (item.isSectionHeader)
79             customComp = *new HeaderItemComponent (item.text);
80 
81         if (customComp != nullptr)
82         {
83             setItem (*customComp, &item);
84             addAndMakeVisible (*customComp);
85         }
86 
87         parent.addAndMakeVisible (this);
88 
89         updateShortcutKeyDescription();
90 
91         int itemW = 80;
92         int itemH = 16;
93         getIdealSize (itemW, itemH, standardItemHeight);
94         setSize (itemW, jlimit (1, 600, itemH));
95 
96         addMouseListener (&parent, false);
97     }
98 
~ItemComponentjuce::PopupMenu::HelperClasses::ItemComponent99     ~ItemComponent() override
100     {
101         if (customComp != nullptr)
102             setItem (*customComp, nullptr);
103 
104         removeChildComponent (customComp.get());
105     }
106 
getIdealSizejuce::PopupMenu::HelperClasses::ItemComponent107     void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight)
108     {
109         if (customComp != nullptr)
110             customComp->getIdealSize (idealWidth, idealHeight);
111         else
112             getLookAndFeel().getIdealPopupMenuItemSize (getTextForMeasurement(),
113                                                         item.isSeparator,
114                                                         standardItemHeight,
115                                                         idealWidth, idealHeight);
116     }
117 
paintjuce::PopupMenu::HelperClasses::ItemComponent118     void paint (Graphics& g) override
119     {
120         if (customComp == nullptr)
121             getLookAndFeel().drawPopupMenuItem (g, getLocalBounds(),
122                                                 item.isSeparator,
123                                                 item.isEnabled,
124                                                 isHighlighted,
125                                                 item.isTicked,
126                                                 hasSubMenu (item),
127                                                 item.text,
128                                                 item.shortcutKeyDescription,
129                                                 item.image.get(),
130                                                 getColour (item));
131     }
132 
resizedjuce::PopupMenu::HelperClasses::ItemComponent133     void resized() override
134     {
135         if (auto* child = getChildComponent (0))
136             child->setBounds (getLocalBounds().reduced (getLookAndFeel().getPopupMenuBorderSize(), 0));
137     }
138 
setHighlightedjuce::PopupMenu::HelperClasses::ItemComponent139     void setHighlighted (bool shouldBeHighlighted)
140     {
141         shouldBeHighlighted = shouldBeHighlighted && item.isEnabled;
142 
143         if (isHighlighted != shouldBeHighlighted)
144         {
145             isHighlighted = shouldBeHighlighted;
146 
147             if (customComp != nullptr)
148                 customComp->setHighlighted (shouldBeHighlighted);
149 
150             repaint();
151         }
152     }
153 
154     PopupMenu::Item item;
155 
156 private:
157     // NB: we use a copy of the one from the item info in case we're using our own section comp
158     ReferenceCountedObjectPtr<CustomComponent> customComp;
159     bool isHighlighted = false;
160 
updateShortcutKeyDescriptionjuce::PopupMenu::HelperClasses::ItemComponent161     void updateShortcutKeyDescription()
162     {
163         if (item.commandManager != nullptr
164              && item.itemID != 0
165              && item.shortcutKeyDescription.isEmpty())
166         {
167             String shortcutKey;
168 
169             for (auto& keypress : item.commandManager->getKeyMappings()
170                                     ->getKeyPressesAssignedToCommand (item.itemID))
171             {
172                 auto key = keypress.getTextDescriptionWithIcons();
173 
174                 if (shortcutKey.isNotEmpty())
175                     shortcutKey << ", ";
176 
177                 if (key.length() == 1 && key[0] < 128)
178                     shortcutKey << "shortcut: '" << key << '\'';
179                 else
180                     shortcutKey << key;
181             }
182 
183             item.shortcutKeyDescription = shortcutKey.trim();
184         }
185     }
186 
getTextForMeasurementjuce::PopupMenu::HelperClasses::ItemComponent187     String getTextForMeasurement() const
188     {
189         return item.shortcutKeyDescription.isNotEmpty() ? item.text + "   " + item.shortcutKeyDescription
190                                                         : item.text;
191     }
192 
193     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent)
194 };
195 
196 //==============================================================================
197 struct MenuWindow  : public Component
198 {
MenuWindowjuce::PopupMenu::HelperClasses::MenuWindow199     MenuWindow (const PopupMenu& menu, MenuWindow* parentWindow,
200                 Options opts, bool alignToRectangle, bool shouldDismissOnMouseUp,
201                 ApplicationCommandManager** manager, float parentScaleFactor = 1.0f)
202        : Component ("menu"),
203          parent (parentWindow),
204          options (std::move (opts)),
205          managerOfChosenCommand (manager),
206          componentAttachedTo (options.getTargetComponent()),
207          dismissOnMouseUp (shouldDismissOnMouseUp),
208          windowCreationTime (Time::getMillisecondCounter()),
209          lastFocusedTime (windowCreationTime),
210          timeEnteredCurrentChildComp (windowCreationTime),
211          scaleFactor (parentWindow != nullptr ? parentScaleFactor : 1.0f)
212     {
213         setWantsKeyboardFocus (false);
214         setMouseClickGrabsKeyboardFocus (false);
215         setAlwaysOnTop (true);
216 
217         setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel())
218                                           : menu.lookAndFeel.get());
219         auto& lf = getLookAndFeel();
220 
221         parentComponent = lf.getParentComponentForMenuOptions (options);
222         const_cast<Options&>(options) = options.withParentComponent (parentComponent);
223 
224         if (parentComponent == nullptr && parentWindow == nullptr && lf.shouldPopupMenuScaleWithTargetComponent (options))
225             if (auto* targetComponent = options.getTargetComponent())
226                 scaleFactor = Component::getApproximateScaleFactorForComponent (targetComponent);
227 
228         setOpaque (lf.findColour (PopupMenu::backgroundColourId).isOpaque()
229                      || ! Desktop::canUseSemiTransparentWindows());
230 
231         for (int i = 0; i < menu.items.size(); ++i)
232         {
233             auto& item = menu.items.getReference (i);
234 
235             if (i + 1 < menu.items.size() || ! item.isSeparator)
236                 items.add (new ItemComponent (item, options.getStandardItemHeight(), *this));
237         }
238 
239         auto targetArea = options.getTargetScreenArea() / scaleFactor;
240 
241         calculateWindowPos (targetArea, alignToRectangle);
242         setTopLeftPosition (windowPos.getPosition());
243         updateYPositions();
244 
245         if (auto visibleID = options.getItemThatMustBeVisible())
246         {
247             auto targetPosition = parentComponent != nullptr ? parentComponent->getLocalPoint (nullptr, targetArea.getTopLeft())
248                                                              : targetArea.getTopLeft();
249 
250             auto y = targetPosition.getY() - windowPos.getY();
251             ensureItemIsVisible (visibleID, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1);
252         }
253 
254         resizeToBestWindowPos();
255 
256         if (parentComponent != nullptr)
257         {
258             parentComponent->addChildComponent (this);
259         }
260         else
261         {
262             addToDesktop (ComponentPeer::windowIsTemporary
263                           | ComponentPeer::windowIgnoresKeyPresses
264                           | lf.getMenuWindowFlags());
265 
266             Desktop::getInstance().addGlobalMouseListener (this);
267         }
268 
269         getActiveWindows().add (this);
270         lf.preparePopupMenuWindow (*this);
271 
272         getMouseState (Desktop::getInstance().getMainMouseSource()); // forces creation of a mouse source watcher for the main mouse
273     }
274 
~MenuWindowjuce::PopupMenu::HelperClasses::MenuWindow275     ~MenuWindow() override
276     {
277         getActiveWindows().removeFirstMatchingValue (this);
278         Desktop::getInstance().removeGlobalMouseListener (this);
279         activeSubMenu.reset();
280         items.clear();
281     }
282 
283     //==============================================================================
paintjuce::PopupMenu::HelperClasses::MenuWindow284     void paint (Graphics& g) override
285     {
286         if (isOpaque())
287             g.fillAll (Colours::white);
288 
289         getLookAndFeel().drawPopupMenuBackground (g, getWidth(), getHeight());
290     }
291 
paintOverChildrenjuce::PopupMenu::HelperClasses::MenuWindow292     void paintOverChildren (Graphics& g) override
293     {
294         auto& lf = getLookAndFeel();
295 
296         if (parentComponent != nullptr)
297             lf.drawResizableFrame (g, getWidth(), getHeight(),
298                                    BorderSize<int> (getLookAndFeel().getPopupMenuBorderSize()));
299 
300         if (canScroll())
301         {
302             if (isTopScrollZoneActive())
303                 lf.drawPopupMenuUpDownArrow (g, getWidth(), PopupMenuSettings::scrollZone, true);
304 
305             if (isBottomScrollZoneActive())
306             {
307                 g.setOrigin (0, getHeight() - PopupMenuSettings::scrollZone);
308                 lf.drawPopupMenuUpDownArrow (g, getWidth(), PopupMenuSettings::scrollZone, false);
309             }
310         }
311     }
312 
313     //==============================================================================
314     // hide this and all sub-comps
hidejuce::PopupMenu::HelperClasses::MenuWindow315     void hide (const PopupMenu::Item* item, bool makeInvisible)
316     {
317         if (isVisible())
318         {
319             WeakReference<Component> deletionChecker (this);
320 
321             activeSubMenu.reset();
322             currentChild = nullptr;
323 
324             if (item != nullptr
325                  && item->commandManager != nullptr
326                  && item->itemID != 0)
327             {
328                 *managerOfChosenCommand = item->commandManager;
329             }
330 
331             auto resultID = options.hasWatchedComponentBeenDeleted() ? 0 : getResultItemID (item);
332 
333             exitModalState (resultID);
334 
335             if (makeInvisible && deletionChecker != nullptr)
336                 setVisible (false);
337 
338             if (resultID != 0
339                  && item != nullptr
340                  && item->action != nullptr)
341                 MessageManager::callAsync (item->action);
342         }
343     }
344 
getResultItemIDjuce::PopupMenu::HelperClasses::MenuWindow345     static int getResultItemID (const PopupMenu::Item* item)
346     {
347         if (item == nullptr)
348             return 0;
349 
350         if (auto* cc = item->customCallback.get())
351             if (! cc->menuItemTriggered())
352                 return 0;
353 
354         return item->itemID;
355     }
356 
dismissMenujuce::PopupMenu::HelperClasses::MenuWindow357     void dismissMenu (const PopupMenu::Item* item)
358     {
359         if (parent != nullptr)
360         {
361             parent->dismissMenu (item);
362         }
363         else
364         {
365             if (item != nullptr)
366             {
367                 // need a copy of this on the stack as the one passed in will get deleted during this call
368                 auto mi (*item);
369                 hide (&mi, false);
370             }
371             else
372             {
373                 hide (nullptr, false);
374             }
375         }
376     }
377 
getDesktopScaleFactorjuce::PopupMenu::HelperClasses::MenuWindow378     float getDesktopScaleFactor() const override    { return scaleFactor * Desktop::getInstance().getGlobalScaleFactor(); }
379 
380     //==============================================================================
keyPressedjuce::PopupMenu::HelperClasses::MenuWindow381     bool keyPressed (const KeyPress& key) override
382     {
383         if (key.isKeyCode (KeyPress::downKey))
384         {
385             selectNextItem (MenuSelectionDirection::forwards);
386         }
387         else if (key.isKeyCode (KeyPress::upKey))
388         {
389             selectNextItem (MenuSelectionDirection::backwards);
390         }
391         else if (key.isKeyCode (KeyPress::leftKey))
392         {
393             if (parent != nullptr)
394             {
395                 Component::SafePointer<MenuWindow> parentWindow (parent);
396                 ItemComponent* currentChildOfParent = parentWindow->currentChild;
397 
398                 hide (nullptr, true);
399 
400                 if (parentWindow != nullptr)
401                     parentWindow->setCurrentlyHighlightedChild (currentChildOfParent);
402 
403                 disableTimerUntilMouseMoves();
404             }
405             else if (componentAttachedTo != nullptr)
406             {
407                 componentAttachedTo->keyPressed (key);
408             }
409         }
410         else if (key.isKeyCode (KeyPress::rightKey))
411         {
412             disableTimerUntilMouseMoves();
413 
414             if (showSubMenuFor (currentChild))
415             {
416                 if (isSubMenuVisible())
417                     activeSubMenu->selectNextItem (MenuSelectionDirection::current);
418             }
419             else if (componentAttachedTo != nullptr)
420             {
421                 componentAttachedTo->keyPressed (key);
422             }
423         }
424         else if (key.isKeyCode (KeyPress::returnKey) || key.isKeyCode (KeyPress::spaceKey))
425         {
426             triggerCurrentlyHighlightedItem();
427         }
428         else if (key.isKeyCode (KeyPress::escapeKey))
429         {
430             dismissMenu (nullptr);
431         }
432         else
433         {
434             return false;
435         }
436 
437         return true;
438     }
439 
inputAttemptWhenModaljuce::PopupMenu::HelperClasses::MenuWindow440     void inputAttemptWhenModal() override
441     {
442         WeakReference<Component> deletionChecker (this);
443 
444         for (auto* ms : mouseSourceStates)
445         {
446             ms->timerCallback();
447 
448             if (deletionChecker == nullptr)
449                 return;
450         }
451 
452         if (! isOverAnyMenu())
453         {
454             if (componentAttachedTo != nullptr)
455             {
456                 // we want to dismiss the menu, but if we do it synchronously, then
457                 // the mouse-click will be allowed to pass through. That's good, except
458                 // when the user clicks on the button that originally popped the menu up,
459                 // as they'll expect the menu to go away, and in fact it'll just
460                 // come back. So only dismiss synchronously if they're not on the original
461                 // comp that we're attached to.
462                 auto mousePos = componentAttachedTo->getMouseXYRelative();
463 
464                 if (componentAttachedTo->reallyContains (mousePos, true))
465                 {
466                     postCommandMessage (PopupMenuSettings::dismissCommandId); // dismiss asynchronously
467                     return;
468                 }
469             }
470 
471             dismissMenu (nullptr);
472         }
473     }
474 
handleCommandMessagejuce::PopupMenu::HelperClasses::MenuWindow475     void handleCommandMessage (int commandId) override
476     {
477         Component::handleCommandMessage (commandId);
478 
479         if (commandId == PopupMenuSettings::dismissCommandId)
480             dismissMenu (nullptr);
481     }
482 
483     //==============================================================================
mouseMovejuce::PopupMenu::HelperClasses::MenuWindow484     void mouseMove  (const MouseEvent& e) override    { handleMouseEvent (e); }
mouseDownjuce::PopupMenu::HelperClasses::MenuWindow485     void mouseDown  (const MouseEvent& e) override    { handleMouseEvent (e); }
mouseDragjuce::PopupMenu::HelperClasses::MenuWindow486     void mouseDrag  (const MouseEvent& e) override    { handleMouseEvent (e); }
mouseUpjuce::PopupMenu::HelperClasses::MenuWindow487     void mouseUp    (const MouseEvent& e) override    { handleMouseEvent (e); }
488 
mouseWheelMovejuce::PopupMenu::HelperClasses::MenuWindow489     void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
490     {
491         alterChildYPos (roundToInt (-10.0f * wheel.deltaY * PopupMenuSettings::scrollZone));
492     }
493 
handleMouseEventjuce::PopupMenu::HelperClasses::MenuWindow494     void handleMouseEvent (const MouseEvent& e)
495     {
496         getMouseState (e.source).handleMouseEvent (e);
497     }
498 
windowIsStillValidjuce::PopupMenu::HelperClasses::MenuWindow499     bool windowIsStillValid()
500     {
501         if (! isVisible())
502             return false;
503 
504         if (componentAttachedTo != options.getTargetComponent())
505         {
506             dismissMenu (nullptr);
507             return false;
508         }
509 
510         if (auto* currentlyModalWindow = dynamic_cast<MenuWindow*> (Component::getCurrentlyModalComponent()))
511             if (! treeContains (currentlyModalWindow))
512                 return false;
513 
514         return true;
515     }
516 
getActiveWindowsjuce::PopupMenu::HelperClasses::MenuWindow517     static Array<MenuWindow*>& getActiveWindows()
518     {
519         static Array<MenuWindow*> activeMenuWindows;
520         return activeMenuWindows;
521     }
522 
getMouseStatejuce::PopupMenu::HelperClasses::MenuWindow523     MouseSourceState& getMouseState (MouseInputSource source)
524     {
525         MouseSourceState* mouseState = nullptr;
526 
527         for (auto* ms : mouseSourceStates)
528         {
529             if      (ms->source == source)                        mouseState = ms;
530             else if (ms->source.getType() != source.getType())    ms->stopTimer();
531         }
532 
533         if (mouseState == nullptr)
534         {
535             mouseState = new MouseSourceState (*this, source);
536             mouseSourceStates.add (mouseState);
537         }
538 
539         return *mouseState;
540     }
541 
542     //==============================================================================
isOverAnyMenujuce::PopupMenu::HelperClasses::MenuWindow543     bool isOverAnyMenu() const
544     {
545         return parent != nullptr ? parent->isOverAnyMenu()
546                                  : isOverChildren();
547     }
548 
isOverChildrenjuce::PopupMenu::HelperClasses::MenuWindow549     bool isOverChildren() const
550     {
551         return isVisible()
552                 && (isAnyMouseOver() || (activeSubMenu != nullptr && activeSubMenu->isOverChildren()));
553     }
554 
isAnyMouseOverjuce::PopupMenu::HelperClasses::MenuWindow555     bool isAnyMouseOver() const
556     {
557         for (auto* ms : mouseSourceStates)
558             if (ms->isOver())
559                 return true;
560 
561         return false;
562     }
563 
treeContainsjuce::PopupMenu::HelperClasses::MenuWindow564     bool treeContains (const MenuWindow* const window) const noexcept
565     {
566         auto* mw = this;
567 
568         while (mw->parent != nullptr)
569             mw = mw->parent;
570 
571         while (mw != nullptr)
572         {
573             if (mw == window)
574                 return true;
575 
576             mw = mw->activeSubMenu.get();
577         }
578 
579         return false;
580     }
581 
doesAnyJuceCompHaveFocusjuce::PopupMenu::HelperClasses::MenuWindow582     bool doesAnyJuceCompHaveFocus()
583     {
584         bool anyFocused = Process::isForegroundProcess();
585 
586         if (anyFocused && Component::getCurrentlyFocusedComponent() == nullptr)
587         {
588             // because no component at all may have focus, our test here will
589             // only be triggered when something has focus and then loses it.
590             anyFocused = ! hasAnyJuceCompHadFocus;
591 
592             for (int i = ComponentPeer::getNumPeers(); --i >= 0;)
593             {
594                 if (ComponentPeer::getPeer (i)->isFocused())
595                 {
596                     anyFocused = true;
597                     hasAnyJuceCompHadFocus = true;
598                     break;
599                 }
600             }
601         }
602 
603         return anyFocused;
604     }
605 
606     //==============================================================================
getParentAreajuce::PopupMenu::HelperClasses::MenuWindow607     Rectangle<int> getParentArea (Point<int> targetPoint, Component* relativeTo = nullptr)
608     {
609         if (relativeTo != nullptr)
610             targetPoint = relativeTo->localPointToGlobal (targetPoint);
611 
612         auto parentArea = Desktop::getInstance().getDisplays().findDisplayForPoint (targetPoint * scaleFactor)
613                               #if JUCE_MAC || JUCE_ANDROID
614                                .userArea;
615                               #else
616                                .totalArea; // on windows, don't stop the menu overlapping the taskbar
617                               #endif
618 
619         if (parentComponent == nullptr)
620             return parentArea;
621 
622         return parentComponent->getLocalArea (nullptr,
623                                               parentComponent->getScreenBounds()
624                                                     .reduced (getLookAndFeel().getPopupMenuBorderSize())
625                                                     .getIntersection (parentArea));
626     }
627 
calculateWindowPosjuce::PopupMenu::HelperClasses::MenuWindow628     void calculateWindowPos (Rectangle<int> target, const bool alignToRectangle)
629     {
630         auto parentArea = getParentArea (target.getCentre()) / scaleFactor;
631 
632         if (parentComponent != nullptr)
633             target = parentComponent->getLocalArea (nullptr, target).getIntersection (parentArea);
634 
635         auto maxMenuHeight = parentArea.getHeight() - 24;
636 
637         int x, y, widthToUse, heightToUse;
638         layoutMenuItems (parentArea.getWidth() - 24, maxMenuHeight, widthToUse, heightToUse);
639 
640         if (alignToRectangle)
641         {
642             x = target.getX();
643 
644             auto spaceUnder = parentArea.getBottom() - target.getBottom();
645             auto spaceOver = target.getY() - parentArea.getY();
646             auto bufferHeight = 30;
647 
648             if (options.getPreferredPopupDirection() == Options::PopupDirection::upwards)
649                 y = (heightToUse < spaceOver - bufferHeight  || spaceOver >= spaceUnder) ? target.getY() - heightToUse
650                                                                                          : target.getBottom();
651             else
652                 y = (heightToUse < spaceUnder - bufferHeight || spaceUnder >= spaceOver) ? target.getBottom()
653                                                                                          : target.getY() - heightToUse;
654         }
655         else
656         {
657             bool tendTowardsRight = target.getCentreX() < parentArea.getCentreX();
658 
659             if (parent != nullptr)
660             {
661                 if (parent->parent != nullptr)
662                 {
663                     const bool parentGoingRight = (parent->getX() + parent->getWidth() / 2
664                                                     > parent->parent->getX() + parent->parent->getWidth() / 2);
665 
666                     if (parentGoingRight && target.getRight() + widthToUse < parentArea.getRight() - 4)
667                         tendTowardsRight = true;
668                     else if ((! parentGoingRight) && target.getX() > widthToUse + 4)
669                         tendTowardsRight = false;
670                 }
671                 else if (target.getRight() + widthToUse < parentArea.getRight() - 32)
672                 {
673                     tendTowardsRight = true;
674                 }
675             }
676 
677             auto biggestSpace = jmax (parentArea.getRight() - target.getRight(),
678                                       target.getX() - parentArea.getX()) - 32;
679 
680             if (biggestSpace < widthToUse)
681             {
682                 layoutMenuItems (biggestSpace + target.getWidth() / 3, maxMenuHeight, widthToUse, heightToUse);
683 
684                 if (numColumns > 1)
685                     layoutMenuItems (biggestSpace - 4, maxMenuHeight, widthToUse, heightToUse);
686 
687                 tendTowardsRight = (parentArea.getRight() - target.getRight()) >= (target.getX() - parentArea.getX());
688             }
689 
690             x = tendTowardsRight ? jmin (parentArea.getRight() - widthToUse - 4, target.getRight())
691                                  : jmax (parentArea.getX() + 4, target.getX() - widthToUse);
692 
693             if (getLookAndFeel().getPopupMenuBorderSize() == 0) // workaround for dismissing the window on mouse up when border size is 0
694                 x += tendTowardsRight ? 1 : -1;
695 
696             y = target.getCentreY() > parentArea.getCentreY() ? jmax (parentArea.getY(), target.getBottom() - heightToUse)
697                                                               : target.getY();
698         }
699 
700         x = jmax (parentArea.getX() + 1, jmin (parentArea.getRight()  - (widthToUse  + 6), x));
701         y = jmax (parentArea.getY() + 1, jmin (parentArea.getBottom() - (heightToUse + 6), y));
702 
703         windowPos.setBounds (x, y, widthToUse, heightToUse);
704 
705         // sets this flag if it's big enough to obscure any of its parent menus
706         hideOnExit = parent != nullptr
707                       && parent->windowPos.intersects (windowPos.expanded (-4, -4));
708     }
709 
layoutMenuItemsjuce::PopupMenu::HelperClasses::MenuWindow710     void layoutMenuItems (const int maxMenuW, const int maxMenuH, int& width, int& height)
711     {
712         numColumns = options.getMinimumNumColumns();
713         contentHeight = 0;
714 
715         auto maximumNumColumns = options.getMaximumNumColumns() > 0 ? options.getMaximumNumColumns() : 7;
716 
717         for (;;)
718         {
719             auto totalW = workOutBestSize (maxMenuW);
720 
721             if (totalW > maxMenuW)
722             {
723                 numColumns = jmax (1, numColumns - 1);
724                 workOutBestSize (maxMenuW); // to update col widths
725                 break;
726             }
727 
728             if (totalW > maxMenuW / 2
729                  || contentHeight < maxMenuH
730                  || numColumns >= maximumNumColumns)
731                 break;
732 
733             ++numColumns;
734         }
735 
736         auto actualH = jmin (contentHeight, maxMenuH);
737 
738         needsToScroll = contentHeight > actualH;
739 
740         width = updateYPositions();
741         height = actualH + getLookAndFeel().getPopupMenuBorderSize() * 2;
742     }
743 
workOutBestSizejuce::PopupMenu::HelperClasses::MenuWindow744     int workOutBestSize (const int maxMenuW)
745     {
746         int totalW = 0;
747         contentHeight = 0;
748         int childNum = 0;
749 
750         for (int col = 0; col < numColumns; ++col)
751         {
752             int colW = options.getStandardItemHeight(), colH = 0;
753 
754             auto numChildren = jmin (items.size() - childNum,
755                                      (items.size() + numColumns - 1) / numColumns);
756 
757             for (int i = numChildren; --i >= 0;)
758             {
759                 colW = jmax (colW, items.getUnchecked (childNum + i)->getWidth());
760                 colH += items.getUnchecked (childNum + i)->getHeight();
761             }
762 
763             colW = jmin (maxMenuW / jmax (1, numColumns - 2), colW + getLookAndFeel().getPopupMenuBorderSize() * 2);
764 
765             columnWidths.set (col, colW);
766             totalW += colW;
767             contentHeight = jmax (contentHeight, colH);
768 
769             childNum += numChildren;
770         }
771 
772         // width must never be larger than the screen
773         auto minWidth = jmin (maxMenuW, options.getMinimumWidth());
774 
775         if (totalW < minWidth)
776         {
777             totalW = minWidth;
778 
779             for (int col = 0; col < numColumns; ++col)
780                 columnWidths.set (0, totalW / numColumns);
781         }
782 
783         return totalW;
784     }
785 
ensureItemIsVisiblejuce::PopupMenu::HelperClasses::MenuWindow786     void ensureItemIsVisible (const int itemID, int wantedY)
787     {
788         jassert (itemID != 0);
789 
790         for (int i = items.size(); --i >= 0;)
791         {
792             if (auto* m = items.getUnchecked (i))
793             {
794                 if (m->item.itemID == itemID
795                      && windowPos.getHeight() > PopupMenuSettings::scrollZone * 4)
796                 {
797                     auto currentY = m->getY();
798 
799                     if (wantedY > 0 || currentY < 0 || m->getBottom() > windowPos.getHeight())
800                     {
801                         if (wantedY < 0)
802                             wantedY = jlimit (PopupMenuSettings::scrollZone,
803                                               jmax (PopupMenuSettings::scrollZone,
804                                                     windowPos.getHeight() - (PopupMenuSettings::scrollZone + m->getHeight())),
805                                               currentY);
806 
807                         auto parentArea = getParentArea (windowPos.getPosition(), parentComponent) / scaleFactor;
808                         auto deltaY = wantedY - currentY;
809 
810                         windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()),
811                                            jmin (windowPos.getHeight(), parentArea.getHeight()));
812 
813                         auto newY = jlimit (parentArea.getY(),
814                                             parentArea.getBottom() - windowPos.getHeight(),
815                                             windowPos.getY() + deltaY);
816 
817                         deltaY -= newY - windowPos.getY();
818 
819                         childYOffset -= deltaY;
820                         windowPos.setPosition (windowPos.getX(), newY);
821 
822                         updateYPositions();
823                     }
824 
825                     break;
826                 }
827             }
828         }
829     }
830 
resizeToBestWindowPosjuce::PopupMenu::HelperClasses::MenuWindow831     void resizeToBestWindowPos()
832     {
833         auto r = windowPos;
834 
835         if (childYOffset < 0)
836         {
837             r = r.withTop (r.getY() - childYOffset);
838         }
839         else if (childYOffset > 0)
840         {
841             auto spaceAtBottom = r.getHeight() - (contentHeight - childYOffset);
842 
843             if (spaceAtBottom > 0)
844                 r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom);
845         }
846 
847         setBounds (r);
848         updateYPositions();
849     }
850 
alterChildYPosjuce::PopupMenu::HelperClasses::MenuWindow851     void alterChildYPos (int delta)
852     {
853         if (canScroll())
854         {
855             childYOffset += delta;
856 
857             if (delta < 0)
858                 childYOffset = jmax (childYOffset, 0);
859             else if (delta > 0)
860                 childYOffset = jmin (childYOffset,
861                                      contentHeight - windowPos.getHeight() + getLookAndFeel().getPopupMenuBorderSize());
862 
863             updateYPositions();
864         }
865         else
866         {
867             childYOffset = 0;
868         }
869 
870         resizeToBestWindowPos();
871         repaint();
872     }
873 
updateYPositionsjuce::PopupMenu::HelperClasses::MenuWindow874     int updateYPositions()
875     {
876         int x = 0;
877         int childNum = 0;
878 
879         for (int col = 0; col < numColumns; ++col)
880         {
881             auto numChildren = jmin (items.size() - childNum,
882                                      (items.size() + numColumns - 1) / numColumns);
883 
884             auto colW = columnWidths[col];
885             auto y = getLookAndFeel().getPopupMenuBorderSize() - (childYOffset + (getY() - windowPos.getY()));
886 
887             for (int i = 0; i < numChildren; ++i)
888             {
889                 auto* c = items.getUnchecked (childNum + i);
890                 c->setBounds (x, y, colW, c->getHeight());
891                 y += c->getHeight();
892             }
893 
894             x += colW;
895             childNum += numChildren;
896         }
897 
898         return x;
899     }
900 
setCurrentlyHighlightedChildjuce::PopupMenu::HelperClasses::MenuWindow901     void setCurrentlyHighlightedChild (ItemComponent* child)
902     {
903         if (currentChild != nullptr)
904             currentChild->setHighlighted (false);
905 
906         currentChild = child;
907 
908         if (currentChild != nullptr)
909         {
910             currentChild->setHighlighted (true);
911             timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter();
912         }
913     }
914 
isSubMenuVisiblejuce::PopupMenu::HelperClasses::MenuWindow915     bool isSubMenuVisible() const noexcept          { return activeSubMenu != nullptr && activeSubMenu->isVisible(); }
916 
showSubMenuForjuce::PopupMenu::HelperClasses::MenuWindow917     bool showSubMenuFor (ItemComponent* childComp)
918     {
919         activeSubMenu.reset();
920 
921         if (childComp != nullptr
922              && hasActiveSubMenu (childComp->item))
923         {
924             activeSubMenu.reset (new HelperClasses::MenuWindow (*(childComp->item.subMenu), this,
925                                                                 options.withTargetScreenArea (childComp->getScreenBounds())
926                                                                        .withMinimumWidth (0)
927                                                                        .withTargetComponent (nullptr)
928                                                                        .withParentComponent (parentComponent),
929                                                                 false, dismissOnMouseUp, managerOfChosenCommand, scaleFactor));
930 
931             activeSubMenu->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
932             activeSubMenu->enterModalState (false);
933             activeSubMenu->toFront (false);
934             return true;
935         }
936 
937         return false;
938     }
939 
triggerCurrentlyHighlightedItemjuce::PopupMenu::HelperClasses::MenuWindow940     void triggerCurrentlyHighlightedItem()
941     {
942         if (currentChild != nullptr
943              && canBeTriggered (currentChild->item)
944              && (currentChild->item.customComponent == nullptr
945                   || currentChild->item.customComponent->isTriggeredAutomatically()))
946         {
947             dismissMenu (&currentChild->item);
948         }
949     }
950 
951     enum class MenuSelectionDirection
952     {
953         forwards,
954         backwards,
955         current
956     };
957 
selectNextItemjuce::PopupMenu::HelperClasses::MenuWindow958     void selectNextItem (MenuSelectionDirection direction)
959     {
960         disableTimerUntilMouseMoves();
961 
962         auto start = [&]
963         {
964             auto index = items.indexOf (currentChild);
965 
966             if (index >= 0)
967                 return index;
968 
969             return direction == MenuSelectionDirection::backwards ? items.size() - 1
970                                                                   : 0;
971         }();
972 
973         auto preIncrement = (direction != MenuSelectionDirection::current && currentChild != nullptr);
974 
975         for (int i = items.size(); --i >= 0;)
976         {
977             if (preIncrement)
978                 start += (direction == MenuSelectionDirection::backwards ? -1 : 1);
979 
980             if (auto* mic = items.getUnchecked ((start + items.size()) % items.size()))
981             {
982                 if (canBeTriggered (mic->item) || hasActiveSubMenu (mic->item))
983                 {
984                     setCurrentlyHighlightedChild (mic);
985                     return;
986                 }
987             }
988 
989             if (! preIncrement)
990                 preIncrement = true;
991         }
992     }
993 
disableTimerUntilMouseMovesjuce::PopupMenu::HelperClasses::MenuWindow994     void disableTimerUntilMouseMoves()
995     {
996         disableMouseMoves = true;
997 
998         if (parent != nullptr)
999             parent->disableTimerUntilMouseMoves();
1000     }
1001 
canScrolljuce::PopupMenu::HelperClasses::MenuWindow1002     bool canScroll() const noexcept                 { return childYOffset != 0 || needsToScroll; }
isTopScrollZoneActivejuce::PopupMenu::HelperClasses::MenuWindow1003     bool isTopScrollZoneActive() const noexcept     { return canScroll() && childYOffset > 0; }
isBottomScrollZoneActivejuce::PopupMenu::HelperClasses::MenuWindow1004     bool isBottomScrollZoneActive() const noexcept  { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); }
1005 
1006     //==============================================================================
1007     MenuWindow* parent;
1008     const Options options;
1009     OwnedArray<ItemComponent> items;
1010     ApplicationCommandManager** managerOfChosenCommand;
1011     WeakReference<Component> componentAttachedTo;
1012     Component* parentComponent = nullptr;
1013     Rectangle<int> windowPos;
1014     bool hasBeenOver = false, needsToScroll = false;
1015     bool dismissOnMouseUp, hideOnExit = false, disableMouseMoves = false, hasAnyJuceCompHadFocus = false;
1016     int numColumns = 0, contentHeight = 0, childYOffset = 0;
1017     Component::SafePointer<ItemComponent> currentChild;
1018     std::unique_ptr<MenuWindow> activeSubMenu;
1019     Array<int> columnWidths;
1020     uint32 windowCreationTime, lastFocusedTime, timeEnteredCurrentChildComp;
1021     OwnedArray<MouseSourceState> mouseSourceStates;
1022     float scaleFactor;
1023 
1024     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow)
1025 };
1026 
1027 //==============================================================================
1028 class MouseSourceState  : public Timer
1029 {
1030 public:
MouseSourceState(MenuWindow & w,MouseInputSource s)1031     MouseSourceState (MenuWindow& w, MouseInputSource s)
1032         : window (w), source (s), lastScrollTime (Time::getMillisecondCounter())
1033     {
1034         startTimerHz (20);
1035     }
1036 
handleMouseEvent(const MouseEvent & e)1037     void handleMouseEvent (const MouseEvent& e)
1038     {
1039         if (! window.windowIsStillValid())
1040             return;
1041 
1042         startTimerHz (20);
1043         handleMousePosition (e.getScreenPosition());
1044     }
1045 
timerCallback()1046     void timerCallback() override
1047     {
1048        #if JUCE_WINDOWS
1049         // touch and pen devices on Windows send an offscreen mouse move after mouse up events
1050         // but we don't want to forward these on as they will dismiss the menu
1051         if ((source.isTouch() || source.isPen()) && ! isValidMousePosition())
1052             return;
1053        #endif
1054 
1055         if (window.windowIsStillValid())
1056             handleMousePosition (source.getScreenPosition().roundToInt());
1057     }
1058 
isOver() const1059     bool isOver() const
1060     {
1061         return window.reallyContains (window.getLocalPoint (nullptr, source.getScreenPosition()).roundToInt(), true);
1062     }
1063 
1064     MenuWindow& window;
1065     MouseInputSource source;
1066 
1067 private:
1068     Point<int> lastMousePos;
1069     double scrollAcceleration = 0;
1070     uint32 lastScrollTime, lastMouseMoveTime = 0;
1071     bool isDown = false;
1072 
handleMousePosition(Point<int> globalMousePos)1073     void handleMousePosition (Point<int> globalMousePos)
1074     {
1075         auto localMousePos = window.getLocalPoint (nullptr, globalMousePos);
1076         auto timeNow = Time::getMillisecondCounter();
1077 
1078         if (timeNow > window.timeEnteredCurrentChildComp + 100
1079              && window.reallyContains (localMousePos, true)
1080              && window.currentChild != nullptr
1081              && ! (window.disableMouseMoves || window.isSubMenuVisible()))
1082         {
1083             window.showSubMenuFor (window.currentChild);
1084         }
1085 
1086         highlightItemUnderMouse (globalMousePos, localMousePos, timeNow);
1087 
1088         const bool overScrollArea = scrollIfNecessary (localMousePos, timeNow);
1089         const bool isOverAny = window.isOverAnyMenu();
1090 
1091         if (window.hideOnExit && window.hasBeenOver && ! isOverAny)
1092             window.hide (nullptr, true);
1093         else
1094             checkButtonState (localMousePos, timeNow, isDown, overScrollArea, isOverAny);
1095     }
1096 
checkButtonState(Point<int> localMousePos,const uint32 timeNow,const bool wasDown,const bool overScrollArea,const bool isOverAny)1097     void checkButtonState (Point<int> localMousePos, const uint32 timeNow,
1098                            const bool wasDown, const bool overScrollArea, const bool isOverAny)
1099     {
1100         isDown = window.hasBeenOver
1101                     && (ModifierKeys::currentModifiers.isAnyMouseButtonDown()
1102                          || ComponentPeer::getCurrentModifiersRealtime().isAnyMouseButtonDown());
1103 
1104         if (! window.doesAnyJuceCompHaveFocus())
1105         {
1106             if (timeNow > window.lastFocusedTime + 10)
1107             {
1108                 PopupMenuSettings::menuWasHiddenBecauseOfAppChange = true;
1109                 window.dismissMenu (nullptr);
1110                 // Note: This object may have been deleted by the previous call.
1111             }
1112         }
1113         else if (wasDown && timeNow > window.windowCreationTime + 250
1114                    && ! (isDown || overScrollArea))
1115         {
1116             if (window.reallyContains (localMousePos, true))
1117                 window.triggerCurrentlyHighlightedItem();
1118             else if ((window.hasBeenOver || ! window.dismissOnMouseUp) && ! isOverAny)
1119                 window.dismissMenu (nullptr);
1120 
1121             // Note: This object may have been deleted by the previous call.
1122         }
1123         else
1124         {
1125             window.lastFocusedTime = timeNow;
1126         }
1127     }
1128 
highlightItemUnderMouse(Point<int> globalMousePos,Point<int> localMousePos,const uint32 timeNow)1129     void highlightItemUnderMouse (Point<int> globalMousePos, Point<int> localMousePos, const uint32 timeNow)
1130     {
1131         if (globalMousePos != lastMousePos || timeNow > lastMouseMoveTime + 350)
1132         {
1133             const bool isMouseOver = window.reallyContains (localMousePos, true);
1134 
1135             if (isMouseOver)
1136                 window.hasBeenOver = true;
1137 
1138             if (lastMousePos.getDistanceFrom (globalMousePos) > 2)
1139             {
1140                 lastMouseMoveTime = timeNow;
1141 
1142                 if (window.disableMouseMoves && isMouseOver)
1143                     window.disableMouseMoves = false;
1144             }
1145 
1146             if (window.disableMouseMoves || (window.activeSubMenu != nullptr && window.activeSubMenu->isOverChildren()))
1147                 return;
1148 
1149             const bool isMovingTowardsMenu = isMouseOver && globalMousePos != lastMousePos
1150                                                 && isMovingTowardsSubmenu (globalMousePos);
1151 
1152             lastMousePos = globalMousePos;
1153 
1154             if (! isMovingTowardsMenu)
1155             {
1156                 auto* c = window.getComponentAt (localMousePos);
1157 
1158                 if (c == &window)
1159                     c = nullptr;
1160 
1161                 auto* itemUnderMouse = dynamic_cast<ItemComponent*> (c);
1162 
1163                 if (itemUnderMouse == nullptr && c != nullptr)
1164                     itemUnderMouse = c->findParentComponentOfClass<ItemComponent>();
1165 
1166                 if (itemUnderMouse != window.currentChild
1167                       && (isMouseOver || (window.activeSubMenu == nullptr) || ! window.activeSubMenu->isVisible()))
1168                 {
1169                     if (isMouseOver && (c != nullptr) && (window.activeSubMenu != nullptr))
1170                         window.activeSubMenu->hide (nullptr, true);
1171 
1172                     if (! isMouseOver)
1173                         itemUnderMouse = nullptr;
1174 
1175                     window.setCurrentlyHighlightedChild (itemUnderMouse);
1176                 }
1177             }
1178         }
1179     }
1180 
isMovingTowardsSubmenu(Point<int> newGlobalPos) const1181     bool isMovingTowardsSubmenu (Point<int> newGlobalPos) const
1182     {
1183         if (window.activeSubMenu == nullptr)
1184             return false;
1185 
1186         // try to intelligently guess whether the user is moving the mouse towards a currently-open
1187         // submenu. To do this, look at whether the mouse stays inside a triangular region that
1188         // extends from the last mouse pos to the submenu's rectangle..
1189 
1190         auto itemScreenBounds = window.activeSubMenu->getScreenBounds();
1191         auto subX = (float) itemScreenBounds.getX();
1192 
1193         auto oldGlobalPos = lastMousePos;
1194 
1195         if (itemScreenBounds.getX() > window.getX())
1196         {
1197             oldGlobalPos -= Point<int> (2, 0);  // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels
1198         }
1199         else
1200         {
1201             oldGlobalPos += Point<int> (2, 0);
1202             subX += (float) itemScreenBounds.getWidth();
1203         }
1204 
1205         Path areaTowardsSubMenu;
1206         areaTowardsSubMenu.addTriangle ((float) oldGlobalPos.x, (float) oldGlobalPos.y,
1207                                         subX, (float) itemScreenBounds.getY(),
1208                                         subX, (float) itemScreenBounds.getBottom());
1209 
1210         return areaTowardsSubMenu.contains (newGlobalPos.toFloat());
1211     }
1212 
scrollIfNecessary(Point<int> localMousePos,const uint32 timeNow)1213     bool scrollIfNecessary (Point<int> localMousePos, const uint32 timeNow)
1214     {
1215         if (window.canScroll()
1216              && isPositiveAndBelow (localMousePos.x, window.getWidth())
1217              && (isPositiveAndBelow (localMousePos.y, window.getHeight()) || source.isDragging()))
1218         {
1219             if (window.isTopScrollZoneActive() && localMousePos.y < PopupMenuSettings::scrollZone)
1220                 return scroll (timeNow, -1);
1221 
1222             if (window.isBottomScrollZoneActive() && localMousePos.y > window.getHeight() - PopupMenuSettings::scrollZone)
1223                 return scroll (timeNow, 1);
1224         }
1225 
1226         scrollAcceleration = 1.0;
1227         return false;
1228     }
1229 
scroll(const uint32 timeNow,const int direction)1230     bool scroll (const uint32 timeNow, const int direction)
1231     {
1232         if (timeNow > lastScrollTime + 20)
1233         {
1234             scrollAcceleration = jmin (4.0, scrollAcceleration * 1.04);
1235             int amount = 0;
1236 
1237             for (int i = 0; i < window.items.size() && amount == 0; ++i)
1238                 amount = ((int) scrollAcceleration) * window.items.getUnchecked (i)->getHeight();
1239 
1240             window.alterChildYPos (amount * direction);
1241             lastScrollTime = timeNow;
1242         }
1243 
1244         return true;
1245     }
1246 
1247    #if JUCE_WINDOWS
isValidMousePosition()1248     bool isValidMousePosition()
1249     {
1250         auto screenPos = source.getScreenPosition();
1251         auto localPos = (window.activeSubMenu == nullptr) ? window.getLocalPoint (nullptr, screenPos)
1252                                                           : window.activeSubMenu->getLocalPoint (nullptr, screenPos);
1253 
1254         if (localPos.x < 0 && localPos.y < 0)
1255             return false;
1256 
1257         return true;
1258     }
1259    #endif
1260 
1261     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MouseSourceState)
1262 };
1263 
1264 //==============================================================================
1265 struct NormalComponentWrapper : public PopupMenu::CustomComponent
1266 {
NormalComponentWrapperjuce::PopupMenu::HelperClasses::NormalComponentWrapper1267     NormalComponentWrapper (Component& comp, int w, int h, bool triggerMenuItemAutomaticallyWhenClicked)
1268         : PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked),
1269           width (w), height (h)
1270     {
1271         addAndMakeVisible (comp);
1272     }
1273 
getIdealSizejuce::PopupMenu::HelperClasses::NormalComponentWrapper1274     void getIdealSize (int& idealWidth, int& idealHeight) override
1275     {
1276         idealWidth = width;
1277         idealHeight = height;
1278     }
1279 
resizedjuce::PopupMenu::HelperClasses::NormalComponentWrapper1280     void resized() override
1281     {
1282         if (auto* child = getChildComponent (0))
1283             child->setBounds (getLocalBounds());
1284     }
1285 
1286     const int width, height;
1287 
1288     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper)
1289 };
1290 
1291 };
1292 
1293 //==============================================================================
PopupMenu(const PopupMenu & other)1294 PopupMenu::PopupMenu (const PopupMenu& other)
1295     : items (other.items),
1296       lookAndFeel (other.lookAndFeel)
1297 {
1298 }
1299 
operator =(const PopupMenu & other)1300 PopupMenu& PopupMenu::operator= (const PopupMenu& other)
1301 {
1302     if (this != &other)
1303     {
1304         items = other.items;
1305         lookAndFeel = other.lookAndFeel;
1306     }
1307 
1308     return *this;
1309 }
1310 
PopupMenu(PopupMenu && other)1311 PopupMenu::PopupMenu (PopupMenu&& other) noexcept
1312     : items (std::move (other.items)),
1313       lookAndFeel (std::move (other.lookAndFeel))
1314 {
1315 }
1316 
operator =(PopupMenu && other)1317 PopupMenu& PopupMenu::operator= (PopupMenu&& other) noexcept
1318 {
1319     items = std::move (other.items);
1320     lookAndFeel = other.lookAndFeel;
1321     return *this;
1322 }
1323 
1324 PopupMenu::~PopupMenu() = default;
1325 
clear()1326 void PopupMenu::clear()
1327 {
1328     items.clear();
1329 }
1330 
1331 //==============================================================================
1332 PopupMenu::Item::Item() = default;
Item(String t)1333 PopupMenu::Item::Item (String t) : text (std::move (t)), itemID (-1) {}
1334 
1335 PopupMenu::Item::Item (Item&&) = default;
1336 PopupMenu::Item& PopupMenu::Item::operator= (Item&&) = default;
1337 
Item(const Item & other)1338 PopupMenu::Item::Item (const Item& other)
1339   : text (other.text),
1340     itemID (other.itemID),
1341     action (other.action),
1342     subMenu (createCopyIfNotNull (other.subMenu.get())),
1343     image (other.image != nullptr ? other.image->createCopy() : nullptr),
1344     customComponent (other.customComponent),
1345     customCallback (other.customCallback),
1346     commandManager (other.commandManager),
1347     shortcutKeyDescription (other.shortcutKeyDescription),
1348     colour (other.colour),
1349     isEnabled (other.isEnabled),
1350     isTicked (other.isTicked),
1351     isSeparator (other.isSeparator),
1352     isSectionHeader (other.isSectionHeader)
1353 {
1354 }
1355 
operator =(const Item & other)1356 PopupMenu::Item& PopupMenu::Item::operator= (const Item& other)
1357 {
1358     text = other.text;
1359     itemID = other.itemID;
1360     action = other.action;
1361     subMenu.reset (createCopyIfNotNull (other.subMenu.get()));
1362     image = other.image != nullptr ? other.image->createCopy() : std::unique_ptr<Drawable>();
1363     customComponent = other.customComponent;
1364     customCallback = other.customCallback;
1365     commandManager = other.commandManager;
1366     shortcutKeyDescription = other.shortcutKeyDescription;
1367     colour = other.colour;
1368     isEnabled = other.isEnabled;
1369     isTicked = other.isTicked;
1370     isSeparator = other.isSeparator;
1371     isSectionHeader = other.isSectionHeader;
1372     return *this;
1373 }
1374 
setTicked(bool shouldBeTicked)1375 PopupMenu::Item& PopupMenu::Item::setTicked (bool shouldBeTicked) & noexcept
1376 {
1377     isTicked = shouldBeTicked;
1378     return *this;
1379 }
1380 
setEnabled(bool shouldBeEnabled)1381 PopupMenu::Item& PopupMenu::Item::setEnabled (bool shouldBeEnabled) & noexcept
1382 {
1383     isEnabled = shouldBeEnabled;
1384     return *this;
1385 }
1386 
setAction(std::function<void ()> newAction)1387 PopupMenu::Item& PopupMenu::Item::setAction (std::function<void()> newAction) & noexcept
1388 {
1389     action = std::move (newAction);
1390     return *this;
1391 }
1392 
setID(int newID)1393 PopupMenu::Item& PopupMenu::Item::setID (int newID) & noexcept
1394 {
1395     itemID = newID;
1396     return *this;
1397 }
1398 
setColour(Colour newColour)1399 PopupMenu::Item& PopupMenu::Item::setColour (Colour newColour) & noexcept
1400 {
1401     colour = newColour;
1402     return *this;
1403 }
1404 
setCustomComponent(ReferenceCountedObjectPtr<CustomComponent> comp)1405 PopupMenu::Item& PopupMenu::Item::setCustomComponent (ReferenceCountedObjectPtr<CustomComponent> comp) & noexcept
1406 {
1407     customComponent = comp;
1408     return *this;
1409 }
1410 
setImage(std::unique_ptr<Drawable> newImage)1411 PopupMenu::Item& PopupMenu::Item::setImage (std::unique_ptr<Drawable> newImage) & noexcept
1412 {
1413     image = std::move (newImage);
1414     return *this;
1415 }
1416 
setTicked(bool shouldBeTicked)1417 PopupMenu::Item&& PopupMenu::Item::setTicked (bool shouldBeTicked) && noexcept
1418 {
1419     isTicked = shouldBeTicked;
1420     return std::move (*this);
1421 }
1422 
setEnabled(bool shouldBeEnabled)1423 PopupMenu::Item&& PopupMenu::Item::setEnabled (bool shouldBeEnabled) && noexcept
1424 {
1425     isEnabled = shouldBeEnabled;
1426     return std::move (*this);
1427 }
1428 
setAction(std::function<void ()> newAction)1429 PopupMenu::Item&& PopupMenu::Item::setAction (std::function<void()> newAction) && noexcept
1430 {
1431     action = std::move (newAction);
1432     return std::move (*this);
1433 }
1434 
setID(int newID)1435 PopupMenu::Item&& PopupMenu::Item::setID (int newID) && noexcept
1436 {
1437     itemID = newID;
1438     return std::move (*this);
1439 }
1440 
setColour(Colour newColour)1441 PopupMenu::Item&& PopupMenu::Item::setColour (Colour newColour) && noexcept
1442 {
1443     colour = newColour;
1444     return std::move (*this);
1445 }
1446 
setCustomComponent(ReferenceCountedObjectPtr<CustomComponent> comp)1447 PopupMenu::Item&& PopupMenu::Item::setCustomComponent (ReferenceCountedObjectPtr<CustomComponent> comp) && noexcept
1448 {
1449     customComponent = comp;
1450     return std::move (*this);
1451 }
1452 
setImage(std::unique_ptr<Drawable> newImage)1453 PopupMenu::Item&& PopupMenu::Item::setImage (std::unique_ptr<Drawable> newImage) && noexcept
1454 {
1455     image = std::move (newImage);
1456     return std::move (*this);
1457 }
1458 
addItem(Item newItem)1459 void PopupMenu::addItem (Item newItem)
1460 {
1461     // An ID of 0 is used as a return value to indicate that the user
1462     // didn't pick anything, so you shouldn't use it as the ID for an item..
1463     jassert (newItem.itemID != 0
1464               || newItem.isSeparator || newItem.isSectionHeader
1465               || newItem.subMenu != nullptr);
1466 
1467     items.add (std::move (newItem));
1468 }
1469 
addItem(String itemText,std::function<void ()> action)1470 void PopupMenu::addItem (String itemText, std::function<void()> action)
1471 {
1472     addItem (std::move (itemText), true, false, std::move (action));
1473 }
1474 
addItem(String itemText,bool isActive,bool isTicked,std::function<void ()> action)1475 void PopupMenu::addItem (String itemText, bool isActive, bool isTicked, std::function<void()> action)
1476 {
1477     Item i (std::move (itemText));
1478     i.action = std::move (action);
1479     i.isEnabled = isActive;
1480     i.isTicked = isTicked;
1481     addItem (std::move (i));
1482 }
1483 
addItem(int itemResultID,String itemText,bool isActive,bool isTicked)1484 void PopupMenu::addItem (int itemResultID, String itemText, bool isActive, bool isTicked)
1485 {
1486     Item i (std::move (itemText));
1487     i.itemID = itemResultID;
1488     i.isEnabled = isActive;
1489     i.isTicked = isTicked;
1490     addItem (std::move (i));
1491 }
1492 
createDrawableFromImage(const Image & im)1493 static std::unique_ptr<Drawable> createDrawableFromImage (const Image& im)
1494 {
1495     if (im.isValid())
1496     {
1497         auto d = new DrawableImage();
1498         d->setImage (im);
1499         return std::unique_ptr<Drawable> (d);
1500     }
1501 
1502     return {};
1503 }
1504 
addItem(int itemResultID,String itemText,bool isActive,bool isTicked,const Image & iconToUse)1505 void PopupMenu::addItem (int itemResultID, String itemText, bool isActive, bool isTicked, const Image& iconToUse)
1506 {
1507     addItem (itemResultID, std::move (itemText), isActive, isTicked, createDrawableFromImage (iconToUse));
1508 }
1509 
addItem(int itemResultID,String itemText,bool isActive,bool isTicked,std::unique_ptr<Drawable> iconToUse)1510 void PopupMenu::addItem (int itemResultID, String itemText, bool isActive,
1511                          bool isTicked, std::unique_ptr<Drawable> iconToUse)
1512 {
1513     Item i (std::move (itemText));
1514     i.itemID = itemResultID;
1515     i.isEnabled = isActive;
1516     i.isTicked = isTicked;
1517     i.image = std::move (iconToUse);
1518     addItem (std::move (i));
1519 }
1520 
addCommandItem(ApplicationCommandManager * commandManager,const CommandID commandID,String displayName,std::unique_ptr<Drawable> iconToUse)1521 void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager,
1522                                 const CommandID commandID,
1523                                 String displayName,
1524                                 std::unique_ptr<Drawable> iconToUse)
1525 {
1526     jassert (commandManager != nullptr && commandID != 0);
1527 
1528     if (auto* registeredInfo = commandManager->getCommandForID (commandID))
1529     {
1530         ApplicationCommandInfo info (*registeredInfo);
1531         auto* target = commandManager->getTargetForCommand (commandID, info);
1532 
1533         Item i;
1534         i.text = displayName.isNotEmpty() ? std::move (displayName) : info.shortName;
1535         i.itemID = (int) commandID;
1536         i.commandManager = commandManager;
1537         i.isEnabled = target != nullptr && (info.flags & ApplicationCommandInfo::isDisabled) == 0;
1538         i.isTicked = (info.flags & ApplicationCommandInfo::isTicked) != 0;
1539         i.image = std::move (iconToUse);
1540         addItem (std::move (i));
1541     }
1542 }
1543 
addColouredItem(int itemResultID,String itemText,Colour itemTextColour,bool isActive,bool isTicked,std::unique_ptr<Drawable> iconToUse)1544 void PopupMenu::addColouredItem (int itemResultID, String itemText, Colour itemTextColour,
1545                                  bool isActive, bool isTicked, std::unique_ptr<Drawable> iconToUse)
1546 {
1547     Item i (std::move (itemText));
1548     i.itemID = itemResultID;
1549     i.colour = itemTextColour;
1550     i.isEnabled = isActive;
1551     i.isTicked = isTicked;
1552     i.image = std::move (iconToUse);
1553     addItem (std::move (i));
1554 }
1555 
addColouredItem(int itemResultID,String itemText,Colour itemTextColour,bool isActive,bool isTicked,const Image & iconToUse)1556 void PopupMenu::addColouredItem (int itemResultID, String itemText, Colour itemTextColour,
1557                                  bool isActive, bool isTicked, const Image& iconToUse)
1558 {
1559     Item i (std::move (itemText));
1560     i.itemID = itemResultID;
1561     i.colour = itemTextColour;
1562     i.isEnabled = isActive;
1563     i.isTicked = isTicked;
1564     i.image = createDrawableFromImage (iconToUse);
1565     addItem (std::move (i));
1566 }
1567 
addCustomItem(int itemResultID,std::unique_ptr<CustomComponent> cc,std::unique_ptr<const PopupMenu> subMenu)1568 void PopupMenu::addCustomItem (int itemResultID,
1569                                std::unique_ptr<CustomComponent> cc,
1570                                std::unique_ptr<const PopupMenu> subMenu)
1571 {
1572     Item i;
1573     i.itemID = itemResultID;
1574     i.customComponent = cc.release();
1575     i.subMenu.reset (createCopyIfNotNull (subMenu.get()));
1576     addItem (std::move (i));
1577 }
1578 
addCustomItem(int itemResultID,Component & customComponent,int idealWidth,int idealHeight,bool triggerMenuItemAutomaticallyWhenClicked,std::unique_ptr<const PopupMenu> subMenu)1579 void PopupMenu::addCustomItem (int itemResultID,
1580                                Component& customComponent,
1581                                int idealWidth, int idealHeight,
1582                                bool triggerMenuItemAutomaticallyWhenClicked,
1583                                std::unique_ptr<const PopupMenu> subMenu)
1584 {
1585     auto comp = std::make_unique<HelperClasses::NormalComponentWrapper> (customComponent, idealWidth, idealHeight,
1586                                                                          triggerMenuItemAutomaticallyWhenClicked);
1587     addCustomItem (itemResultID, std::move (comp), std::move (subMenu));
1588 }
1589 
addSubMenu(String subMenuName,PopupMenu subMenu,bool isActive)1590 void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive)
1591 {
1592     addSubMenu (std::move (subMenuName), std::move (subMenu), isActive, nullptr, false, 0);
1593 }
1594 
addSubMenu(String subMenuName,PopupMenu subMenu,bool isActive,const Image & iconToUse,bool isTicked,int itemResultID)1595 void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive,
1596                             const Image& iconToUse, bool isTicked, int itemResultID)
1597 {
1598     addSubMenu (std::move (subMenuName), std::move (subMenu), isActive,
1599                 createDrawableFromImage (iconToUse), isTicked, itemResultID);
1600 }
1601 
addSubMenu(String subMenuName,PopupMenu subMenu,bool isActive,std::unique_ptr<Drawable> iconToUse,bool isTicked,int itemResultID)1602 void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive,
1603                             std::unique_ptr<Drawable> iconToUse, bool isTicked, int itemResultID)
1604 {
1605     Item i (std::move (subMenuName));
1606     i.itemID = itemResultID;
1607     i.isEnabled = isActive && (itemResultID != 0 || subMenu.getNumItems() > 0);
1608     i.subMenu.reset (new PopupMenu (std::move (subMenu)));
1609     i.isTicked = isTicked;
1610     i.image = std::move (iconToUse);
1611     addItem (std::move (i));
1612 }
1613 
addSeparator()1614 void PopupMenu::addSeparator()
1615 {
1616     if (items.size() > 0 && ! items.getLast().isSeparator)
1617     {
1618         Item i;
1619         i.isSeparator = true;
1620         addItem (std::move (i));
1621     }
1622 }
1623 
addSectionHeader(String title)1624 void PopupMenu::addSectionHeader (String title)
1625 {
1626     Item i (std::move (title));
1627     i.itemID = 0;
1628     i.isSectionHeader = true;
1629     addItem (std::move (i));
1630 }
1631 
1632 //==============================================================================
Options()1633 PopupMenu::Options::Options()
1634 {
1635     targetArea.setPosition (Desktop::getMousePosition());
1636 }
1637 
withTargetComponent(Component * comp) const1638 PopupMenu::Options PopupMenu::Options::withTargetComponent (Component* comp) const
1639 {
1640     Options o (*this);
1641     o.targetComponent = comp;
1642 
1643     if (comp != nullptr)
1644         o.targetArea = comp->getScreenBounds();
1645 
1646     return o;
1647 }
1648 
withTargetComponent(Component & comp) const1649 PopupMenu::Options PopupMenu::Options::withTargetComponent (Component& comp) const
1650 {
1651     return withTargetComponent (&comp);
1652 }
1653 
withTargetScreenArea(Rectangle<int> area) const1654 PopupMenu::Options PopupMenu::Options::withTargetScreenArea (Rectangle<int> area) const
1655 {
1656     Options o (*this);
1657     o.targetArea = area;
1658     return o;
1659 }
1660 
withDeletionCheck(Component & comp) const1661 PopupMenu::Options PopupMenu::Options::withDeletionCheck (Component& comp) const
1662 {
1663     Options o (*this);
1664     o.componentToWatchForDeletion = &comp;
1665     o.isWatchingForDeletion = true;
1666     return o;
1667 }
1668 
withMinimumWidth(int w) const1669 PopupMenu::Options PopupMenu::Options::withMinimumWidth (int w) const
1670 {
1671     Options o (*this);
1672     o.minWidth = w;
1673     return o;
1674 }
1675 
withMinimumNumColumns(int cols) const1676 PopupMenu::Options PopupMenu::Options::withMinimumNumColumns (int cols) const
1677 {
1678     Options o (*this);
1679     o.minColumns = cols;
1680     return o;
1681 }
1682 
withMaximumNumColumns(int cols) const1683 PopupMenu::Options PopupMenu::Options::withMaximumNumColumns (int cols) const
1684 {
1685     Options o (*this);
1686     o.maxColumns = cols;
1687     return o;
1688 }
1689 
withStandardItemHeight(int height) const1690 PopupMenu::Options PopupMenu::Options::withStandardItemHeight (int height) const
1691 {
1692     Options o (*this);
1693     o.standardHeight = height;
1694     return o;
1695 }
1696 
withItemThatMustBeVisible(int idOfItemToBeVisible) const1697 PopupMenu::Options PopupMenu::Options::withItemThatMustBeVisible (int idOfItemToBeVisible) const
1698 {
1699     Options o (*this);
1700     o.visibleItemID = idOfItemToBeVisible;
1701     return o;
1702 }
1703 
withParentComponent(Component * parent) const1704 PopupMenu::Options PopupMenu::Options::withParentComponent (Component* parent) const
1705 {
1706     Options o (*this);
1707     o.parentComponent = parent;
1708     return o;
1709 }
1710 
withPreferredPopupDirection(PopupDirection direction) const1711 PopupMenu::Options PopupMenu::Options::withPreferredPopupDirection (PopupDirection direction) const
1712 {
1713     Options o (*this);
1714     o.preferredPopupDirection = direction;
1715     return o;
1716 }
1717 
createWindow(const Options & options,ApplicationCommandManager ** managerOfChosenCommand) const1718 Component* PopupMenu::createWindow (const Options& options,
1719                                     ApplicationCommandManager** managerOfChosenCommand) const
1720 {
1721     return items.isEmpty() ? nullptr
1722                            : new HelperClasses::MenuWindow (*this, nullptr, options,
1723                                                             ! options.getTargetScreenArea().isEmpty(),
1724                                                             ModifierKeys::currentModifiers.isAnyMouseButtonDown(),
1725                                                             managerOfChosenCommand);
1726 }
1727 
1728 //==============================================================================
1729 // This invokes any command manager commands and deletes the menu window when it is dismissed
1730 struct PopupMenuCompletionCallback  : public ModalComponentManager::Callback
1731 {
PopupMenuCompletionCallbackjuce::PopupMenuCompletionCallback1732     PopupMenuCompletionCallback()
1733         : prevFocused (Component::getCurrentlyFocusedComponent()),
1734           prevTopLevel (prevFocused != nullptr ? prevFocused->getTopLevelComponent() : nullptr)
1735     {
1736         PopupMenuSettings::menuWasHiddenBecauseOfAppChange = false;
1737     }
1738 
modalStateFinishedjuce::PopupMenuCompletionCallback1739     void modalStateFinished (int result) override
1740     {
1741         if (managerOfChosenCommand != nullptr && result != 0)
1742         {
1743             ApplicationCommandTarget::InvocationInfo info (result);
1744             info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
1745 
1746             managerOfChosenCommand->invoke (info, true);
1747         }
1748 
1749         // (this would be the place to fade out the component, if that's what's required)
1750         component.reset();
1751 
1752         if (! PopupMenuSettings::menuWasHiddenBecauseOfAppChange)
1753         {
1754             if (prevTopLevel != nullptr)
1755                 prevTopLevel->toFront (true);
1756 
1757             if (prevFocused != nullptr && prevFocused->isShowing())
1758                 prevFocused->grabKeyboardFocus();
1759         }
1760     }
1761 
1762     ApplicationCommandManager* managerOfChosenCommand = nullptr;
1763     std::unique_ptr<Component> component;
1764     WeakReference<Component> prevFocused, prevTopLevel;
1765 
1766     JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback)
1767 };
1768 
showWithOptionalCallback(const Options & options,ModalComponentManager::Callback * userCallback,bool canBeModal)1769 int PopupMenu::showWithOptionalCallback (const Options& options,
1770                                          ModalComponentManager::Callback* userCallback,
1771                                          bool canBeModal)
1772 {
1773     std::unique_ptr<ModalComponentManager::Callback> userCallbackDeleter (userCallback);
1774     std::unique_ptr<PopupMenuCompletionCallback> callback (new PopupMenuCompletionCallback());
1775 
1776     if (auto* window = createWindow (options, &(callback->managerOfChosenCommand)))
1777     {
1778         callback->component.reset (window);
1779 
1780         window->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
1781         window->enterModalState (false, userCallbackDeleter.release());
1782         ModalComponentManager::getInstance()->attachCallback (window, callback.release());
1783 
1784         window->toFront (false);  // need to do this after making it modal, or it could
1785                                   // be stuck behind other comps that are already modal..
1786 
1787        #if JUCE_MODAL_LOOPS_PERMITTED
1788         if (userCallback == nullptr && canBeModal)
1789             return window->runModalLoop();
1790        #else
1791         ignoreUnused (canBeModal);
1792         jassert (! (userCallback == nullptr && canBeModal));
1793        #endif
1794     }
1795 
1796     return 0;
1797 }
1798 
1799 //==============================================================================
1800 #if JUCE_MODAL_LOOPS_PERMITTED
showMenu(const Options & options)1801 int PopupMenu::showMenu (const Options& options)
1802 {
1803     return showWithOptionalCallback (options, nullptr, true);
1804 }
1805 #endif
1806 
showMenuAsync(const Options & options)1807 void PopupMenu::showMenuAsync (const Options& options)
1808 {
1809     showWithOptionalCallback (options, nullptr, false);
1810 }
1811 
showMenuAsync(const Options & options,ModalComponentManager::Callback * userCallback)1812 void PopupMenu::showMenuAsync (const Options& options, ModalComponentManager::Callback* userCallback)
1813 {
1814    #if ! JUCE_MODAL_LOOPS_PERMITTED
1815     jassert (userCallback != nullptr);
1816    #endif
1817 
1818     showWithOptionalCallback (options, userCallback, false);
1819 }
1820 
showMenuAsync(const Options & options,std::function<void (int)> userCallback)1821 void PopupMenu::showMenuAsync (const Options& options, std::function<void (int)> userCallback)
1822 {
1823     showWithOptionalCallback (options, ModalCallbackFunction::create (userCallback), false);
1824 }
1825 
1826 //==============================================================================
1827 #if JUCE_MODAL_LOOPS_PERMITTED
show(int itemIDThatMustBeVisible,int minimumWidth,int maximumNumColumns,int standardItemHeight,ModalComponentManager::Callback * callback)1828 int PopupMenu::show (int itemIDThatMustBeVisible, int minimumWidth,
1829                      int maximumNumColumns, int standardItemHeight,
1830                      ModalComponentManager::Callback* callback)
1831 {
1832     return showWithOptionalCallback (Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
1833                                               .withMinimumWidth (minimumWidth)
1834                                               .withMaximumNumColumns (maximumNumColumns)
1835                                               .withStandardItemHeight (standardItemHeight),
1836                                      callback, true);
1837 }
1838 
showAt(Rectangle<int> screenAreaToAttachTo,int itemIDThatMustBeVisible,int minimumWidth,int maximumNumColumns,int standardItemHeight,ModalComponentManager::Callback * callback)1839 int PopupMenu::showAt (Rectangle<int> screenAreaToAttachTo,
1840                        int itemIDThatMustBeVisible, int minimumWidth,
1841                        int maximumNumColumns, int standardItemHeight,
1842                        ModalComponentManager::Callback* callback)
1843 {
1844     return showWithOptionalCallback (Options().withTargetScreenArea (screenAreaToAttachTo)
1845                                               .withItemThatMustBeVisible (itemIDThatMustBeVisible)
1846                                               .withMinimumWidth (minimumWidth)
1847                                               .withMaximumNumColumns (maximumNumColumns)
1848                                               .withStandardItemHeight (standardItemHeight),
1849                                      callback, true);
1850 }
1851 
showAt(Component * componentToAttachTo,int itemIDThatMustBeVisible,int minimumWidth,int maximumNumColumns,int standardItemHeight,ModalComponentManager::Callback * callback)1852 int PopupMenu::showAt (Component* componentToAttachTo,
1853                        int itemIDThatMustBeVisible, int minimumWidth,
1854                        int maximumNumColumns, int standardItemHeight,
1855                        ModalComponentManager::Callback* callback)
1856 {
1857     auto options = Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
1858                             .withMinimumWidth (minimumWidth)
1859                             .withMaximumNumColumns (maximumNumColumns)
1860                             .withStandardItemHeight (standardItemHeight);
1861 
1862     if (componentToAttachTo != nullptr)
1863         options = options.withTargetComponent (componentToAttachTo);
1864 
1865     return showWithOptionalCallback (options, callback, true);
1866 }
1867 #endif
1868 
dismissAllActiveMenus()1869 bool JUCE_CALLTYPE PopupMenu::dismissAllActiveMenus()
1870 {
1871     auto& windows = HelperClasses::MenuWindow::getActiveWindows();
1872     auto numWindows = windows.size();
1873 
1874     for (int i = numWindows; --i >= 0;)
1875     {
1876         if (auto* pmw = windows[i])
1877         {
1878             pmw->setLookAndFeel (nullptr);
1879             pmw->dismissMenu (nullptr);
1880         }
1881     }
1882 
1883     return numWindows > 0;
1884 }
1885 
1886 //==============================================================================
getNumItems() const1887 int PopupMenu::getNumItems() const noexcept
1888 {
1889     int num = 0;
1890 
1891     for (auto& mi : items)
1892         if (! mi.isSeparator)
1893             ++num;
1894 
1895     return num;
1896 }
1897 
containsCommandItem(const int commandID) const1898 bool PopupMenu::containsCommandItem (const int commandID) const
1899 {
1900     for (auto& mi : items)
1901         if ((mi.itemID == commandID && mi.commandManager != nullptr)
1902               || (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID)))
1903             return true;
1904 
1905     return false;
1906 }
1907 
containsAnyActiveItems() const1908 bool PopupMenu::containsAnyActiveItems() const noexcept
1909 {
1910     for (auto& mi : items)
1911     {
1912         if (mi.subMenu != nullptr)
1913         {
1914             if (mi.subMenu->containsAnyActiveItems())
1915                 return true;
1916         }
1917         else if (mi.isEnabled)
1918         {
1919             return true;
1920         }
1921     }
1922 
1923     return false;
1924 }
1925 
setLookAndFeel(LookAndFeel * const newLookAndFeel)1926 void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel)
1927 {
1928     lookAndFeel = newLookAndFeel;
1929 }
1930 
setItem(CustomComponent & c,const Item * itemToUse)1931 void PopupMenu::setItem (CustomComponent& c, const Item* itemToUse)
1932 {
1933     c.item = itemToUse;
1934     c.repaint();
1935 }
1936 
1937 //==============================================================================
CustomComponent(bool autoTrigger)1938 PopupMenu::CustomComponent::CustomComponent (bool autoTrigger)
1939     : triggeredAutomatically (autoTrigger)
1940 {
1941 }
1942 
~CustomComponent()1943 PopupMenu::CustomComponent::~CustomComponent()
1944 {
1945 }
1946 
setHighlighted(bool shouldBeHighlighted)1947 void PopupMenu::CustomComponent::setHighlighted (bool shouldBeHighlighted)
1948 {
1949     isHighlighted = shouldBeHighlighted;
1950     repaint();
1951 }
1952 
triggerMenuItem()1953 void PopupMenu::CustomComponent::triggerMenuItem()
1954 {
1955     if (auto* mic = findParentComponentOfClass<HelperClasses::ItemComponent>())
1956     {
1957         if (auto* pmw = mic->findParentComponentOfClass<HelperClasses::MenuWindow>())
1958         {
1959             pmw->dismissMenu (&mic->item);
1960         }
1961         else
1962         {
1963             // something must have gone wrong with the component hierarchy if this happens..
1964             jassertfalse;
1965         }
1966     }
1967     else
1968     {
1969         // why isn't this component inside a menu? Not much point triggering the item if
1970         // there's no menu.
1971         jassertfalse;
1972     }
1973 }
1974 
1975 //==============================================================================
CustomCallback()1976 PopupMenu::CustomCallback::CustomCallback() {}
~CustomCallback()1977 PopupMenu::CustomCallback::~CustomCallback() {}
1978 
1979 //==============================================================================
MenuItemIterator(const PopupMenu & m,bool recurse)1980 PopupMenu::MenuItemIterator::MenuItemIterator (const PopupMenu& m, bool recurse) : searchRecursively (recurse)
1981 {
1982     index.add (0);
1983     menus.add (&m);
1984 }
1985 
1986 PopupMenu::MenuItemIterator::~MenuItemIterator() = default;
1987 
next()1988 bool PopupMenu::MenuItemIterator::next()
1989 {
1990     if (index.size() == 0 || menus.getLast()->items.size() == 0)
1991         return false;
1992 
1993     currentItem = const_cast<PopupMenu::Item*> (&(menus.getLast()->items.getReference (index.getLast())));
1994 
1995     if (searchRecursively && currentItem->subMenu != nullptr)
1996     {
1997         index.add (0);
1998         menus.add (currentItem->subMenu.get());
1999     }
2000     else
2001     {
2002         index.setUnchecked (index.size() - 1, index.getLast() + 1);
2003     }
2004 
2005     while (index.size() > 0 && index.getLast() >= (int) menus.getLast()->items.size())
2006     {
2007         index.removeLast();
2008         menus.removeLast();
2009 
2010         if (index.size() > 0)
2011             index.setUnchecked (index.size() - 1, index.getLast() + 1);
2012     }
2013 
2014     return true;
2015 }
2016 
getItem() const2017 PopupMenu::Item& PopupMenu::MenuItemIterator::getItem() const
2018 {
2019     jassert (currentItem != nullptr);
2020     return *(currentItem);
2021 }
2022 
2023 } // namespace juce
2024