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 (¤tChild->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 = ∁
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