mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-13 00:04:19 +00:00
Previously, MouseSourceState::checkButtonState would trigger a menu item if the MouseSourceState had observed the mouse button transition from pressed to released while over an item, after more than 250ms had elapsed since creating the menu window. In situations where the main thread was very busy, this timeout could sometimes be reached inside the same mouse click/release gesture. If the menu was created inside a mouse-down, then simply tapping the mouse could sometimes trigger an item from the menu as soon as the menu window appeared. To help avoid accidentally triggering menu items, the menu window now prevents any item from being triggered by the mouse until either the mouse has been released once, or the mouse has moved. Put another way, if the mouse is initially pressed when the menu is shown, it cannot trigger a menu item unless the mouse is moved before it is released.
2420 lines
84 KiB
C++
2420 lines
84 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE framework.
|
|
Copyright (c) Raw Material Software Limited
|
|
|
|
JUCE is an open source framework subject to commercial or open source
|
|
licensing.
|
|
|
|
By downloading, installing, or using the JUCE framework, or combining the
|
|
JUCE framework with any other source code, object code, content or any other
|
|
copyrightable work, you agree to the terms of the JUCE End User Licence
|
|
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
|
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
|
do not agree to the terms of these agreements, we will not license the JUCE
|
|
framework to you, and you must discontinue the installation or download
|
|
process and cease use of the JUCE framework.
|
|
|
|
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
|
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
|
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
|
|
|
Or:
|
|
|
|
You may also use this code under the terms of the AGPLv3:
|
|
https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
|
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
|
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace juce
|
|
{
|
|
|
|
namespace PopupMenuSettings
|
|
{
|
|
const int scrollZone = 24;
|
|
const int dismissCommandId = 0x6287345f;
|
|
|
|
static bool menuWasHiddenBecauseOfAppChange = false;
|
|
}
|
|
|
|
//==============================================================================
|
|
struct PopupMenu::HelperClasses
|
|
{
|
|
|
|
class MouseSourceState;
|
|
struct MenuWindow;
|
|
|
|
static bool canBeTriggered (const PopupMenu::Item& item) noexcept
|
|
{
|
|
return item.isEnabled
|
|
&& item.itemID != 0
|
|
&& ! item.isSectionHeader
|
|
&& (item.customComponent == nullptr || item.customComponent->isTriggeredAutomatically());
|
|
}
|
|
|
|
static bool hasActiveSubMenu (const PopupMenu::Item& item) noexcept
|
|
{
|
|
return item.isEnabled
|
|
&& item.subMenu != nullptr
|
|
&& item.subMenu->items.size() > 0;
|
|
}
|
|
|
|
//==============================================================================
|
|
struct HeaderItemComponent final : public PopupMenu::CustomComponent
|
|
{
|
|
HeaderItemComponent (const String& name, const Options& opts)
|
|
: CustomComponent (false), options (opts)
|
|
{
|
|
setName (name);
|
|
}
|
|
|
|
void paint (Graphics& g) override
|
|
{
|
|
getLookAndFeel().drawPopupMenuSectionHeaderWithOptions (g,
|
|
getLocalBounds(),
|
|
getName(),
|
|
options);
|
|
}
|
|
|
|
void getIdealSize (int& idealWidth, int& idealHeight) override
|
|
{
|
|
getLookAndFeel().getIdealPopupMenuItemSizeWithOptions (getName(),
|
|
false,
|
|
-1,
|
|
idealWidth,
|
|
idealHeight,
|
|
options);
|
|
idealHeight += idealHeight / 2;
|
|
idealWidth += idealWidth / 4;
|
|
}
|
|
|
|
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
|
|
{
|
|
return createIgnoredAccessibilityHandler (*this);
|
|
}
|
|
|
|
const Options& options;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HeaderItemComponent)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct ItemComponent final : public Component
|
|
{
|
|
ItemComponent (const PopupMenu::Item& i, const PopupMenu::Options& o, MenuWindow& parent)
|
|
: item (i), parentWindow (parent), options (o), customComp (i.customComponent)
|
|
{
|
|
if (item.isSectionHeader)
|
|
customComp = *new HeaderItemComponent (item.text, options);
|
|
|
|
if (customComp != nullptr)
|
|
{
|
|
setItem (*customComp, &item);
|
|
addAndMakeVisible (*customComp);
|
|
}
|
|
|
|
parent.addAndMakeVisible (this);
|
|
|
|
updateShortcutKeyDescription();
|
|
|
|
int itemW = 80;
|
|
int itemH = 16;
|
|
getIdealSize (itemW, itemH, options.getStandardItemHeight());
|
|
setSize (itemW, jlimit (1, 600, itemH));
|
|
|
|
addMouseListener (&parent, false);
|
|
}
|
|
|
|
~ItemComponent() override
|
|
{
|
|
if (customComp != nullptr)
|
|
setItem (*customComp, nullptr);
|
|
|
|
removeChildComponent (customComp.get());
|
|
}
|
|
|
|
void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight)
|
|
{
|
|
if (customComp != nullptr)
|
|
customComp->getIdealSize (idealWidth, idealHeight);
|
|
else
|
|
getLookAndFeel().getIdealPopupMenuItemSizeWithOptions (getTextForMeasurement(),
|
|
item.isSeparator,
|
|
standardItemHeight,
|
|
idealWidth, idealHeight,
|
|
options);
|
|
}
|
|
|
|
void paint (Graphics& g) override
|
|
{
|
|
if (customComp == nullptr)
|
|
getLookAndFeel().drawPopupMenuItemWithOptions (g, getLocalBounds(),
|
|
isHighlighted,
|
|
item,
|
|
options);
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
if (auto* child = getChildComponent (0))
|
|
{
|
|
const auto border = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options);
|
|
child->setBounds (getLocalBounds().reduced (border, 0));
|
|
}
|
|
}
|
|
|
|
void setHighlighted (bool shouldBeHighlighted)
|
|
{
|
|
shouldBeHighlighted = shouldBeHighlighted && item.isEnabled;
|
|
|
|
if (isHighlighted != shouldBeHighlighted)
|
|
{
|
|
isHighlighted = shouldBeHighlighted;
|
|
|
|
if (customComp != nullptr)
|
|
customComp->setHighlighted (shouldBeHighlighted);
|
|
|
|
if (isHighlighted)
|
|
if (auto* handler = getAccessibilityHandler())
|
|
handler->grabFocus();
|
|
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
static bool isAccessibilityHandlerRequired (const PopupMenu::Item& item)
|
|
{
|
|
return item.isSectionHeader || hasActiveSubMenu (item) || canBeTriggered (item);
|
|
}
|
|
|
|
PopupMenu::Item item;
|
|
|
|
private:
|
|
//==============================================================================
|
|
class ItemAccessibilityHandler final : public AccessibilityHandler
|
|
{
|
|
public:
|
|
explicit ItemAccessibilityHandler (ItemComponent& itemComponentToWrap)
|
|
: AccessibilityHandler (itemComponentToWrap,
|
|
isAccessibilityHandlerRequired (itemComponentToWrap.item) ? AccessibilityRole::menuItem
|
|
: AccessibilityRole::ignored,
|
|
getAccessibilityActions (*this, itemComponentToWrap)),
|
|
itemComponent (itemComponentToWrap)
|
|
{
|
|
}
|
|
|
|
String getTitle() const override
|
|
{
|
|
return itemComponent.item.text;
|
|
}
|
|
|
|
AccessibleState getCurrentState() const override
|
|
{
|
|
auto state = AccessibilityHandler::getCurrentState().withSelectable()
|
|
.withAccessibleOffscreen();
|
|
|
|
if (hasActiveSubMenu (itemComponent.item))
|
|
{
|
|
state = itemComponent.parentWindow.isSubMenuVisible() ? state.withExpandable().withExpanded()
|
|
: state.withExpandable().withCollapsed();
|
|
}
|
|
|
|
if (itemComponent.item.isTicked)
|
|
state = state.withCheckable().withChecked();
|
|
|
|
return state.isFocused() ? state.withSelected() : state;
|
|
}
|
|
|
|
private:
|
|
static AccessibilityActions getAccessibilityActions (ItemAccessibilityHandler& handler,
|
|
ItemComponent& item)
|
|
{
|
|
auto onFocus = [&item]
|
|
{
|
|
item.parentWindow.disableTimerUntilMouseMoves();
|
|
item.parentWindow.ensureItemComponentIsVisible (item, -1);
|
|
item.parentWindow.setCurrentlyHighlightedChild (&item);
|
|
};
|
|
|
|
auto onToggle = [&handler, &item, onFocus]
|
|
{
|
|
if (handler.getCurrentState().isSelected())
|
|
item.parentWindow.setCurrentlyHighlightedChild (nullptr);
|
|
else
|
|
onFocus();
|
|
};
|
|
|
|
auto actions = AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus))
|
|
.addAction (AccessibilityActionType::toggle, std::move (onToggle));
|
|
|
|
if (canBeTriggered (item.item))
|
|
{
|
|
actions.addAction (AccessibilityActionType::press, [&item]
|
|
{
|
|
item.parentWindow.setCurrentlyHighlightedChild (&item);
|
|
item.parentWindow.triggerCurrentlyHighlightedItem();
|
|
});
|
|
}
|
|
|
|
if (hasActiveSubMenu (item.item))
|
|
{
|
|
auto showSubMenu = [&item]
|
|
{
|
|
item.parentWindow.showSubMenuFor (&item);
|
|
|
|
if (auto* subMenu = item.parentWindow.activeSubMenu.get())
|
|
subMenu->setCurrentlyHighlightedChild (subMenu->items.getFirst());
|
|
};
|
|
|
|
actions.addAction (AccessibilityActionType::press, showSubMenu);
|
|
actions.addAction (AccessibilityActionType::showMenu, showSubMenu);
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
ItemComponent& itemComponent;
|
|
};
|
|
|
|
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
|
|
{
|
|
return item.isSeparator ? createIgnoredAccessibilityHandler (*this)
|
|
: std::make_unique<ItemAccessibilityHandler> (*this);
|
|
}
|
|
|
|
//==============================================================================
|
|
MenuWindow& parentWindow;
|
|
const PopupMenu::Options& options;
|
|
// NB: we use a copy of the one from the item info in case we're using our own section comp
|
|
ReferenceCountedObjectPtr<CustomComponent> customComp;
|
|
bool isHighlighted = false;
|
|
|
|
void updateShortcutKeyDescription()
|
|
{
|
|
if (item.commandManager != nullptr
|
|
&& item.itemID != 0
|
|
&& item.shortcutKeyDescription.isEmpty())
|
|
{
|
|
String shortcutKey;
|
|
|
|
for (auto& keypress : item.commandManager->getKeyMappings()
|
|
->getKeyPressesAssignedToCommand (item.itemID))
|
|
{
|
|
auto key = keypress.getTextDescriptionWithIcons();
|
|
|
|
if (shortcutKey.isNotEmpty())
|
|
shortcutKey << ", ";
|
|
|
|
if (key.length() == 1 && key[0] < 128)
|
|
shortcutKey << "shortcut: '" << key << '\'';
|
|
else
|
|
shortcutKey << key;
|
|
}
|
|
|
|
item.shortcutKeyDescription = shortcutKey.trim();
|
|
}
|
|
}
|
|
|
|
String getTextForMeasurement() const
|
|
{
|
|
return item.shortcutKeyDescription.isNotEmpty() ? item.text + " " + item.shortcutKeyDescription
|
|
: item.text;
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct MenuWindow final : public Component
|
|
{
|
|
MenuWindow (const PopupMenu& menu,
|
|
MenuWindow* parentWindow,
|
|
Options opts,
|
|
bool alignToRectangle,
|
|
ApplicationCommandManager** manager,
|
|
float parentScaleFactor = 1.0f)
|
|
: Component ("menu"),
|
|
parent (parentWindow),
|
|
options (opts.withParentComponent (findNonNullLookAndFeel (menu, parentWindow).getParentComponentForMenuOptions (opts))),
|
|
managerOfChosenCommand (manager),
|
|
componentAttachedTo (options.getTargetComponent()),
|
|
windowCreationTime (Time::getMillisecondCounter()),
|
|
lastFocusedTime (windowCreationTime),
|
|
timeEnteredCurrentChildComp (windowCreationTime),
|
|
scaleFactor (parentWindow != nullptr ? parentScaleFactor : 1.0f)
|
|
{
|
|
setWantsKeyboardFocus (false);
|
|
setMouseClickGrabsKeyboardFocus (false);
|
|
setAlwaysOnTop (true);
|
|
setFocusContainerType (FocusContainerType::focusContainer);
|
|
|
|
setLookAndFeel (findLookAndFeel (menu, parentWindow));
|
|
|
|
auto& lf = getLookAndFeel();
|
|
|
|
if (auto* pc = options.getParentComponent())
|
|
{
|
|
pc->addChildComponent (this);
|
|
}
|
|
else
|
|
{
|
|
const auto shouldDisableAccessibility = [this]
|
|
{
|
|
const auto* compToCheck = parent != nullptr ? parent
|
|
: options.getTargetComponent();
|
|
|
|
return compToCheck != nullptr && ! compToCheck->isAccessible();
|
|
}();
|
|
|
|
if (shouldDisableAccessibility)
|
|
setAccessible (false);
|
|
|
|
addToDesktop (ComponentPeer::windowIsTemporary
|
|
| ComponentPeer::windowIgnoresKeyPresses
|
|
| lf.getMenuWindowFlags());
|
|
|
|
Desktop::getInstance().addGlobalMouseListener (this);
|
|
}
|
|
|
|
if (options.getParentComponent() == nullptr && parentWindow == nullptr && lf.shouldPopupMenuScaleWithTargetComponent (options))
|
|
if (auto* targetComponent = options.getTargetComponent())
|
|
scaleFactor = Component::getApproximateScaleFactorForComponent (targetComponent);
|
|
|
|
setOpaque (lf.findColour (PopupMenu::backgroundColourId).isOpaque()
|
|
|| ! Desktop::canUseSemiTransparentWindows());
|
|
|
|
const auto initialSelectedId = options.getInitiallySelectedItemId();
|
|
|
|
for (int i = 0; i < menu.items.size(); ++i)
|
|
{
|
|
auto& item = menu.items.getReference (i);
|
|
|
|
if (i + 1 < menu.items.size() || ! item.isSeparator)
|
|
{
|
|
auto* child = items.add (new ItemComponent (item, options, *this));
|
|
child->setExplicitFocusOrder (1 + i);
|
|
|
|
if (initialSelectedId != 0 && item.itemID == initialSelectedId)
|
|
setCurrentlyHighlightedChild (child);
|
|
}
|
|
}
|
|
|
|
auto targetArea = options.getTargetScreenArea() / scaleFactor;
|
|
|
|
calculateWindowPos (targetArea, alignToRectangle);
|
|
setTopLeftPosition (windowPos.getPosition());
|
|
|
|
if (auto visibleID = options.getItemThatMustBeVisible())
|
|
{
|
|
for (auto* item : items)
|
|
{
|
|
if (item->item.itemID == visibleID)
|
|
{
|
|
const auto targetPosition = [&]
|
|
{
|
|
if (auto* pc = options.getParentComponent())
|
|
return pc->getLocalPoint (nullptr, targetArea.getTopLeft());
|
|
|
|
return targetArea.getTopLeft();
|
|
}();
|
|
|
|
auto y = targetPosition.getY() - windowPos.getY();
|
|
ensureItemComponentIsVisible (*item, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
resizeToBestWindowPos();
|
|
|
|
getActiveWindows().add (this);
|
|
lf.preparePopupMenuWindow (*this);
|
|
|
|
getMouseState (Desktop::getInstance().getMainMouseSource()); // forces creation of a mouse source watcher for the main mouse
|
|
}
|
|
|
|
~MenuWindow() override
|
|
{
|
|
getActiveWindows().removeFirstMatchingValue (this);
|
|
Desktop::getInstance().removeGlobalMouseListener (this);
|
|
activeSubMenu.reset();
|
|
items.clear();
|
|
}
|
|
|
|
//==============================================================================
|
|
void paint (Graphics& g) override
|
|
{
|
|
if (isOpaque())
|
|
g.fillAll (Colours::white);
|
|
|
|
auto& theme = getLookAndFeel();
|
|
theme.drawPopupMenuBackgroundWithOptions (g, getWidth(), getHeight(), options);
|
|
|
|
if (columnWidths.isEmpty())
|
|
return;
|
|
|
|
const auto separatorWidth = theme.getPopupMenuColumnSeparatorWidthWithOptions (options);
|
|
const auto border = theme.getPopupMenuBorderSizeWithOptions (options);
|
|
|
|
auto currentX = 0;
|
|
|
|
std::for_each (columnWidths.begin(), std::prev (columnWidths.end()), [&] (int width)
|
|
{
|
|
const Rectangle<int> separator (currentX + width,
|
|
border,
|
|
separatorWidth,
|
|
getHeight() - border * 2);
|
|
theme.drawPopupMenuColumnSeparatorWithOptions (g, separator, options);
|
|
currentX += width + separatorWidth;
|
|
});
|
|
}
|
|
|
|
void paintOverChildren (Graphics& g) override
|
|
{
|
|
auto& lf = getLookAndFeel();
|
|
|
|
if (options.getParentComponent())
|
|
lf.drawResizableFrame (g, getWidth(), getHeight(),
|
|
BorderSize<int> (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options)));
|
|
|
|
if (canScroll())
|
|
{
|
|
if (isTopScrollZoneActive())
|
|
{
|
|
lf.drawPopupMenuUpDownArrowWithOptions (g,
|
|
getWidth(),
|
|
PopupMenuSettings::scrollZone,
|
|
true,
|
|
options);
|
|
}
|
|
|
|
if (isBottomScrollZoneActive())
|
|
{
|
|
g.setOrigin (0, getHeight() - PopupMenuSettings::scrollZone);
|
|
lf.drawPopupMenuUpDownArrowWithOptions (g,
|
|
getWidth(),
|
|
PopupMenuSettings::scrollZone,
|
|
false,
|
|
options);
|
|
}
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
// hide this and all sub-comps
|
|
void hide (const PopupMenu::Item* item, bool makeInvisible)
|
|
{
|
|
if (isVisible())
|
|
{
|
|
WeakReference<Component> deletionChecker (this);
|
|
|
|
activeSubMenu.reset();
|
|
currentChild = nullptr;
|
|
|
|
if (item != nullptr
|
|
&& item->commandManager != nullptr
|
|
&& item->itemID != 0)
|
|
{
|
|
*managerOfChosenCommand = item->commandManager;
|
|
}
|
|
|
|
auto resultID = options.hasWatchedComponentBeenDeleted() ? 0 : getResultItemID (item);
|
|
|
|
exitModalState (resultID);
|
|
|
|
if (deletionChecker != nullptr)
|
|
{
|
|
exitingModalState = true;
|
|
|
|
if (makeInvisible)
|
|
setVisible (false);
|
|
}
|
|
|
|
if (resultID != 0
|
|
&& item != nullptr
|
|
&& item->action != nullptr)
|
|
MessageManager::callAsync (item->action);
|
|
}
|
|
}
|
|
|
|
static int getResultItemID (const PopupMenu::Item* item)
|
|
{
|
|
if (item == nullptr)
|
|
return 0;
|
|
|
|
if (auto* cc = item->customCallback.get())
|
|
if (! cc->menuItemTriggered())
|
|
return 0;
|
|
|
|
return item->itemID;
|
|
}
|
|
|
|
void dismissMenu (const PopupMenu::Item* item)
|
|
{
|
|
if (parent != nullptr)
|
|
{
|
|
parent->dismissMenu (item);
|
|
}
|
|
else
|
|
{
|
|
if (item != nullptr)
|
|
{
|
|
// need a copy of this on the stack as the one passed in will get deleted during this call
|
|
auto mi (*item);
|
|
hide (&mi, false);
|
|
}
|
|
else
|
|
{
|
|
hide (nullptr, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
float getDesktopScaleFactor() const override { return scaleFactor * Desktop::getInstance().getGlobalScaleFactor(); }
|
|
|
|
void visibilityChanged() override
|
|
{
|
|
if (! isShowing())
|
|
return;
|
|
|
|
auto* accessibleFocus = [this]
|
|
{
|
|
if (currentChild != nullptr)
|
|
if (auto* childHandler = currentChild->getAccessibilityHandler())
|
|
return childHandler;
|
|
|
|
return getAccessibilityHandler();
|
|
}();
|
|
|
|
if (accessibleFocus != nullptr)
|
|
accessibleFocus->grabFocus();
|
|
}
|
|
|
|
//==============================================================================
|
|
bool keyPressed (const KeyPress& key) override
|
|
{
|
|
if (key.isKeyCode (KeyPress::downKey))
|
|
{
|
|
selectNextItem (MenuSelectionDirection::forwards);
|
|
}
|
|
else if (key.isKeyCode (KeyPress::upKey))
|
|
{
|
|
selectNextItem (MenuSelectionDirection::backwards);
|
|
}
|
|
else if (key.isKeyCode (KeyPress::leftKey))
|
|
{
|
|
if (parent != nullptr)
|
|
{
|
|
Component::SafePointer<MenuWindow> parentWindow (parent);
|
|
ItemComponent* currentChildOfParent = parentWindow->currentChild;
|
|
|
|
hide (nullptr, true);
|
|
|
|
if (parentWindow != nullptr)
|
|
parentWindow->setCurrentlyHighlightedChild (currentChildOfParent);
|
|
|
|
disableTimerUntilMouseMoves();
|
|
}
|
|
else if (componentAttachedTo != nullptr)
|
|
{
|
|
componentAttachedTo->keyPressed (key);
|
|
}
|
|
}
|
|
else if (key.isKeyCode (KeyPress::rightKey))
|
|
{
|
|
disableTimerUntilMouseMoves();
|
|
|
|
if (showSubMenuFor (currentChild))
|
|
{
|
|
if (isSubMenuVisible())
|
|
activeSubMenu->selectNextItem (MenuSelectionDirection::current);
|
|
}
|
|
else if (componentAttachedTo != nullptr)
|
|
{
|
|
componentAttachedTo->keyPressed (key);
|
|
}
|
|
}
|
|
else if (key.isKeyCode (KeyPress::returnKey) || key.isKeyCode (KeyPress::spaceKey))
|
|
{
|
|
triggerCurrentlyHighlightedItem();
|
|
}
|
|
else if (key.isKeyCode (KeyPress::escapeKey))
|
|
{
|
|
dismissMenu (nullptr);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void inputAttemptWhenModal() override
|
|
{
|
|
WeakReference<Component> deletionChecker (this);
|
|
|
|
for (auto* ms : mouseSourceStates)
|
|
{
|
|
ms->timerCallback();
|
|
|
|
if (deletionChecker == nullptr)
|
|
return;
|
|
}
|
|
|
|
if (! isOverAnyMenu())
|
|
{
|
|
if (componentAttachedTo != nullptr)
|
|
{
|
|
// we want to dismiss the menu, but if we do it synchronously, then
|
|
// the mouse-click will be allowed to pass through. That's good, except
|
|
// when the user clicks on the button that originally popped the menu up,
|
|
// as they'll expect the menu to go away, and in fact it'll just
|
|
// come back. So only dismiss synchronously if they're not on the original
|
|
// comp that we're attached to.
|
|
auto mousePos = componentAttachedTo->getMouseXYRelative();
|
|
|
|
if (componentAttachedTo->reallyContains (mousePos, true))
|
|
{
|
|
postCommandMessage (PopupMenuSettings::dismissCommandId); // dismiss asynchronously
|
|
return;
|
|
}
|
|
}
|
|
|
|
dismissMenu (nullptr);
|
|
}
|
|
}
|
|
|
|
void handleCommandMessage (int commandId) override
|
|
{
|
|
Component::handleCommandMessage (commandId);
|
|
|
|
if (commandId == PopupMenuSettings::dismissCommandId)
|
|
dismissMenu (nullptr);
|
|
}
|
|
|
|
//==============================================================================
|
|
void mouseDown (const MouseEvent& e) override { handleMouseEvent (e); }
|
|
|
|
void mouseUp (const MouseEvent& e) override
|
|
{
|
|
SafePointer self { this };
|
|
|
|
handleMouseEvent (e);
|
|
|
|
// Check whether this menu was deleted as a result of the mouse being released.
|
|
if (self == nullptr)
|
|
return;
|
|
|
|
// If the mouse was down when the menu was created, releasing the mouse should
|
|
// not trigger the item under the mouse, because we might still be handling the click
|
|
// that caused the menu to show in the first place. Once the mouse has been released once,
|
|
// then the user must have clicked the mouse again, so they are attempting to trigger or
|
|
// dismiss the menu.
|
|
mouseUpCanTrigger |= true;
|
|
}
|
|
|
|
// Any move/drag after the menu is created will allow the mouse to trigger a highlighted item
|
|
void mouseDrag (const MouseEvent& e) override { mouseUpCanTrigger |= true; handleMouseEvent (e); }
|
|
void mouseMove (const MouseEvent& e) override { mouseUpCanTrigger |= true; handleMouseEvent (e); }
|
|
|
|
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
|
|
{
|
|
alterChildYPos (roundToInt (-10.0f * wheel.deltaY * PopupMenuSettings::scrollZone));
|
|
}
|
|
|
|
bool windowIsStillValid()
|
|
{
|
|
if (! isVisible())
|
|
return false;
|
|
|
|
if (componentAttachedTo != options.getTargetComponent())
|
|
{
|
|
dismissMenu (nullptr);
|
|
return false;
|
|
}
|
|
|
|
if (auto* currentlyModalWindow = dynamic_cast<MenuWindow*> (Component::getCurrentlyModalComponent()))
|
|
if (! treeContains (currentlyModalWindow))
|
|
return false;
|
|
|
|
if (exitingModalState)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static Array<MenuWindow*>& getActiveWindows()
|
|
{
|
|
static Array<MenuWindow*> activeMenuWindows;
|
|
return activeMenuWindows;
|
|
}
|
|
|
|
MouseSourceState& getMouseState (MouseInputSource source)
|
|
{
|
|
MouseSourceState* mouseState = nullptr;
|
|
|
|
for (auto* ms : mouseSourceStates)
|
|
{
|
|
if (ms->source == source) mouseState = ms;
|
|
else if (ms->source.getType() != source.getType()) ms->stopTimer();
|
|
}
|
|
|
|
if (mouseState == nullptr)
|
|
{
|
|
mouseState = new MouseSourceState (*this, source);
|
|
mouseSourceStates.add (mouseState);
|
|
}
|
|
|
|
return *mouseState;
|
|
}
|
|
|
|
//==============================================================================
|
|
bool isOverAnyMenu() const
|
|
{
|
|
return parent != nullptr ? parent->isOverAnyMenu()
|
|
: isOverChildren();
|
|
}
|
|
|
|
bool isOverChildren() const
|
|
{
|
|
return isVisible()
|
|
&& (isAnyMouseOver() || (activeSubMenu != nullptr && activeSubMenu->isOverChildren()));
|
|
}
|
|
|
|
bool isAnyMouseOver() const
|
|
{
|
|
for (auto* ms : mouseSourceStates)
|
|
if (ms->isOver())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool treeContains (const MenuWindow* const window) const noexcept
|
|
{
|
|
auto* mw = this;
|
|
|
|
while (mw->parent != nullptr)
|
|
mw = mw->parent;
|
|
|
|
while (mw != nullptr)
|
|
{
|
|
if (mw == window)
|
|
return true;
|
|
|
|
mw = mw->activeSubMenu.get();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool doesAnyJuceCompHaveFocus()
|
|
{
|
|
if (! detail::WindowingHelpers::isForegroundOrEmbeddedProcess (componentAttachedTo))
|
|
return false;
|
|
|
|
if (Component::getCurrentlyFocusedComponent() != nullptr)
|
|
return true;
|
|
|
|
for (int i = ComponentPeer::getNumPeers(); --i >= 0;)
|
|
{
|
|
if (ComponentPeer::getPeer (i)->isFocused())
|
|
{
|
|
hasAnyJuceCompHadFocus = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return ! hasAnyJuceCompHadFocus;
|
|
}
|
|
|
|
//==============================================================================
|
|
Rectangle<int> getParentArea (Point<int> targetPoint, Component* relativeTo = nullptr)
|
|
{
|
|
if (relativeTo != nullptr)
|
|
targetPoint = relativeTo->localPointToGlobal (targetPoint);
|
|
|
|
auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (targetPoint * scaleFactor);
|
|
auto parentArea = display->userArea.getIntersection (display->safeAreaInsets.subtractedFrom (display->totalArea));
|
|
|
|
if (auto* pc = options.getParentComponent())
|
|
{
|
|
return pc->getLocalArea (nullptr,
|
|
pc->getScreenBounds()
|
|
.reduced (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options))
|
|
.getIntersection (parentArea));
|
|
}
|
|
|
|
return parentArea;
|
|
}
|
|
|
|
void calculateWindowPos (Rectangle<int> target, const bool alignToRectangle)
|
|
{
|
|
auto parentArea = getParentArea (target.getCentre()) / scaleFactor;
|
|
|
|
if (auto* pc = options.getParentComponent())
|
|
target = pc->getLocalArea (nullptr, target).getIntersection (parentArea);
|
|
|
|
auto maxMenuHeight = parentArea.getHeight() - 24;
|
|
|
|
int x, y, widthToUse, heightToUse;
|
|
layoutMenuItems (parentArea.getWidth() - 24, maxMenuHeight, widthToUse, heightToUse);
|
|
|
|
if (alignToRectangle)
|
|
{
|
|
x = target.getX();
|
|
|
|
auto spaceUnder = parentArea.getBottom() - target.getBottom();
|
|
auto spaceOver = target.getY() - parentArea.getY();
|
|
auto bufferHeight = 30;
|
|
|
|
if (options.getPreferredPopupDirection() == Options::PopupDirection::upwards)
|
|
y = (heightToUse < spaceOver - bufferHeight || spaceOver >= spaceUnder) ? target.getY() - heightToUse
|
|
: target.getBottom();
|
|
else
|
|
y = (heightToUse < spaceUnder - bufferHeight || spaceUnder >= spaceOver) ? target.getBottom()
|
|
: target.getY() - heightToUse;
|
|
}
|
|
else
|
|
{
|
|
bool tendTowardsRight = target.getCentreX() < parentArea.getCentreX();
|
|
|
|
if (parent != nullptr)
|
|
{
|
|
if (parent->parent != nullptr)
|
|
{
|
|
const bool parentGoingRight = (parent->getX() + parent->getWidth() / 2
|
|
> parent->parent->getX() + parent->parent->getWidth() / 2);
|
|
|
|
if (parentGoingRight && target.getRight() + widthToUse < parentArea.getRight() - 4)
|
|
tendTowardsRight = true;
|
|
else if ((! parentGoingRight) && target.getX() > widthToUse + 4)
|
|
tendTowardsRight = false;
|
|
}
|
|
else if (target.getRight() + widthToUse < parentArea.getRight() - 32)
|
|
{
|
|
tendTowardsRight = true;
|
|
}
|
|
}
|
|
|
|
auto biggestSpace = jmax (parentArea.getRight() - target.getRight(),
|
|
target.getX() - parentArea.getX()) - 32;
|
|
|
|
if (biggestSpace < widthToUse)
|
|
{
|
|
layoutMenuItems (biggestSpace + target.getWidth() / 3, maxMenuHeight, widthToUse, heightToUse);
|
|
|
|
if (numColumns > 1)
|
|
layoutMenuItems (biggestSpace - 4, maxMenuHeight, widthToUse, heightToUse);
|
|
|
|
tendTowardsRight = (parentArea.getRight() - target.getRight()) >= (target.getX() - parentArea.getX());
|
|
}
|
|
|
|
x = tendTowardsRight ? jmin (parentArea.getRight() - widthToUse - 4, target.getRight())
|
|
: jmax (parentArea.getX() + 4, target.getX() - widthToUse);
|
|
|
|
if (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) == 0) // workaround for dismissing the window on mouse up when border size is 0
|
|
x += tendTowardsRight ? 1 : -1;
|
|
|
|
const auto border = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options);
|
|
y = target.getCentreY() > parentArea.getCentreY() ? jmax (parentArea.getY(), target.getBottom() - heightToUse) + border
|
|
: target.getY() - border;
|
|
}
|
|
|
|
x = jmax (parentArea.getX() + 1, jmin (parentArea.getRight() - (widthToUse + 6), x));
|
|
y = jmax (parentArea.getY() + 1, jmin (parentArea.getBottom() - (heightToUse + 6), y));
|
|
|
|
windowPos.setBounds (x, y, widthToUse, heightToUse);
|
|
|
|
// sets this flag if it's big enough to obscure any of its parent menus
|
|
hideOnExit = parent != nullptr
|
|
&& parent->windowPos.intersects (windowPos.expanded (-4, -4));
|
|
}
|
|
|
|
void layoutMenuItems (const int maxMenuW, const int maxMenuH, int& width, int& height)
|
|
{
|
|
// Ensure we don't try to add an empty column after the final item
|
|
if (auto* last = items.getLast())
|
|
last->item.shouldBreakAfter = false;
|
|
|
|
const auto isBreak = [] (const ItemComponent* item) { return item->item.shouldBreakAfter; };
|
|
const auto numBreaks = static_cast<int> (std::count_if (items.begin(), items.end(), isBreak));
|
|
numColumns = numBreaks + 1;
|
|
|
|
if (numBreaks == 0)
|
|
insertColumnBreaks (maxMenuW, maxMenuH);
|
|
|
|
workOutManualSize (maxMenuW);
|
|
height = jmin (contentHeight, maxMenuH);
|
|
|
|
needsToScroll = contentHeight > height;
|
|
|
|
width = updateYPositions();
|
|
}
|
|
|
|
void insertColumnBreaks (const int maxMenuW, const int maxMenuH)
|
|
{
|
|
numColumns = options.getMinimumNumColumns();
|
|
contentHeight = 0;
|
|
|
|
auto maximumNumColumns = options.getMaximumNumColumns() > 0 ? options.getMaximumNumColumns() : 7;
|
|
|
|
for (;;)
|
|
{
|
|
auto totalW = workOutBestSize (maxMenuW);
|
|
|
|
if (totalW > maxMenuW)
|
|
{
|
|
numColumns = jmax (1, numColumns - 1);
|
|
workOutBestSize (maxMenuW); // to update col widths
|
|
break;
|
|
}
|
|
|
|
if (totalW > maxMenuW / 2
|
|
|| contentHeight < maxMenuH
|
|
|| numColumns >= maximumNumColumns)
|
|
break;
|
|
|
|
++numColumns;
|
|
}
|
|
|
|
const auto itemsPerColumn = (items.size() + numColumns - 1) / numColumns;
|
|
|
|
for (auto i = 0;; i += itemsPerColumn)
|
|
{
|
|
const auto breakIndex = i + itemsPerColumn - 1;
|
|
|
|
if (breakIndex >= items.size())
|
|
break;
|
|
|
|
items[breakIndex]->item.shouldBreakAfter = true;
|
|
}
|
|
|
|
if (! items.isEmpty())
|
|
(*std::prev (items.end()))->item.shouldBreakAfter = false;
|
|
}
|
|
|
|
int correctColumnWidths (const int maxMenuW)
|
|
{
|
|
auto totalW = std::accumulate (columnWidths.begin(), columnWidths.end(), 0);
|
|
const auto minWidth = jmin (maxMenuW, options.getMinimumWidth());
|
|
|
|
if (totalW < minWidth)
|
|
{
|
|
totalW = minWidth;
|
|
|
|
for (auto& column : columnWidths)
|
|
column = totalW / numColumns;
|
|
}
|
|
|
|
return totalW;
|
|
}
|
|
|
|
void workOutManualSize (const int maxMenuW)
|
|
{
|
|
contentHeight = 0;
|
|
columnWidths.clear();
|
|
|
|
for (auto it = items.begin(), end = items.end(); it != end;)
|
|
{
|
|
const auto isBreak = [] (const ItemComponent* item) { return item->item.shouldBreakAfter; };
|
|
const auto nextBreak = std::find_if (it, end, isBreak);
|
|
const auto columnEnd = nextBreak == end ? end : std::next (nextBreak);
|
|
|
|
const auto getMaxWidth = [] (int acc, const ItemComponent* item) { return jmax (acc, item->getWidth()); };
|
|
const auto colW = std::accumulate (it, columnEnd, options.getStandardItemHeight(), getMaxWidth);
|
|
const auto adjustedColW = jmin (maxMenuW / jmax (1, numColumns - 2),
|
|
colW + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2);
|
|
|
|
const auto sumHeight = [] (int acc, const ItemComponent* item) { return acc + item->getHeight(); };
|
|
const auto colH = std::accumulate (it, columnEnd, 0, sumHeight);
|
|
|
|
contentHeight = jmax (contentHeight, colH);
|
|
columnWidths.add (adjustedColW);
|
|
it = columnEnd;
|
|
}
|
|
|
|
contentHeight += getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2;
|
|
|
|
correctColumnWidths (maxMenuW);
|
|
}
|
|
|
|
int workOutBestSize (const int maxMenuW)
|
|
{
|
|
contentHeight = 0;
|
|
int childNum = 0;
|
|
|
|
for (int col = 0; col < numColumns; ++col)
|
|
{
|
|
int colW = options.getStandardItemHeight(), colH = 0;
|
|
|
|
auto numChildren = jmin (items.size() - childNum,
|
|
(items.size() + numColumns - 1) / numColumns);
|
|
|
|
for (int i = numChildren; --i >= 0;)
|
|
{
|
|
colW = jmax (colW, items.getUnchecked (childNum + i)->getWidth());
|
|
colH += items.getUnchecked (childNum + i)->getHeight();
|
|
}
|
|
|
|
colW = jmin (maxMenuW / jmax (1, numColumns - 2),
|
|
colW + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2);
|
|
|
|
columnWidths.set (col, colW);
|
|
contentHeight = jmax (contentHeight, colH);
|
|
|
|
childNum += numChildren;
|
|
}
|
|
|
|
return correctColumnWidths (maxMenuW);
|
|
}
|
|
|
|
void ensureItemComponentIsVisible (const ItemComponent& itemComp, int wantedY)
|
|
{
|
|
if (windowPos.getHeight() > PopupMenuSettings::scrollZone * 4)
|
|
{
|
|
auto currentY = itemComp.getY();
|
|
|
|
if (wantedY > 0 || currentY < 0 || itemComp.getBottom() > windowPos.getHeight())
|
|
{
|
|
if (wantedY < 0)
|
|
wantedY = jlimit (PopupMenuSettings::scrollZone,
|
|
jmax (PopupMenuSettings::scrollZone,
|
|
windowPos.getHeight() - (PopupMenuSettings::scrollZone + itemComp.getHeight())),
|
|
currentY);
|
|
|
|
auto parentArea = getParentArea (windowPos.getPosition(), options.getParentComponent()) / scaleFactor;
|
|
auto deltaY = wantedY - currentY;
|
|
|
|
windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()),
|
|
jmin (windowPos.getHeight(), parentArea.getHeight()));
|
|
|
|
auto newY = jlimit (parentArea.getY(),
|
|
parentArea.getBottom() - windowPos.getHeight(),
|
|
windowPos.getY() + deltaY);
|
|
|
|
deltaY -= newY - windowPos.getY();
|
|
|
|
childYOffset -= deltaY;
|
|
windowPos.setPosition (windowPos.getX(), newY);
|
|
|
|
updateYPositions();
|
|
}
|
|
}
|
|
}
|
|
|
|
void resizeToBestWindowPos()
|
|
{
|
|
auto r = windowPos;
|
|
|
|
if (childYOffset < 0)
|
|
{
|
|
r = r.withTop (r.getY() - childYOffset);
|
|
}
|
|
else if (childYOffset > 0)
|
|
{
|
|
auto spaceAtBottom = r.getHeight() - (contentHeight - childYOffset);
|
|
|
|
if (spaceAtBottom > 0)
|
|
r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom);
|
|
}
|
|
|
|
setBounds (r);
|
|
updateYPositions();
|
|
}
|
|
|
|
void alterChildYPos (int delta)
|
|
{
|
|
if (canScroll())
|
|
{
|
|
childYOffset += delta;
|
|
|
|
childYOffset = [&]
|
|
{
|
|
if (delta < 0)
|
|
return jmax (childYOffset, 0);
|
|
|
|
if (delta > 0)
|
|
{
|
|
const auto limit = contentHeight
|
|
- windowPos.getHeight()
|
|
+ getLookAndFeel().getPopupMenuBorderSizeWithOptions (options);
|
|
return jmin (childYOffset, limit);
|
|
}
|
|
|
|
return childYOffset;
|
|
}();
|
|
|
|
updateYPositions();
|
|
}
|
|
else
|
|
{
|
|
childYOffset = 0;
|
|
}
|
|
|
|
resizeToBestWindowPos();
|
|
repaint();
|
|
}
|
|
|
|
int updateYPositions()
|
|
{
|
|
const auto separatorWidth = getLookAndFeel().getPopupMenuColumnSeparatorWidthWithOptions (options);
|
|
const auto initialY = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options)
|
|
- (childYOffset + (getY() - windowPos.getY()));
|
|
|
|
auto col = 0;
|
|
auto x = 0;
|
|
auto y = initialY;
|
|
|
|
for (const auto& item : items)
|
|
{
|
|
jassert (col < columnWidths.size());
|
|
const auto columnWidth = columnWidths[col];
|
|
item->setBounds (x, y, columnWidth, item->getHeight());
|
|
y += item->getHeight();
|
|
|
|
if (item->item.shouldBreakAfter)
|
|
{
|
|
col += 1;
|
|
x += columnWidth + separatorWidth;
|
|
y = initialY;
|
|
}
|
|
}
|
|
|
|
return std::accumulate (columnWidths.begin(), columnWidths.end(), 0)
|
|
+ (separatorWidth * (columnWidths.size() - 1));
|
|
}
|
|
|
|
void setCurrentlyHighlightedChild (ItemComponent* child)
|
|
{
|
|
if (currentChild != nullptr)
|
|
currentChild->setHighlighted (false);
|
|
|
|
currentChild = child;
|
|
|
|
if (currentChild != nullptr)
|
|
{
|
|
currentChild->setHighlighted (true);
|
|
timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter();
|
|
}
|
|
|
|
if (auto* handler = getAccessibilityHandler())
|
|
handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged);
|
|
}
|
|
|
|
bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); }
|
|
|
|
bool showSubMenuFor (ItemComponent* childComp)
|
|
{
|
|
activeSubMenu.reset();
|
|
|
|
if (childComp == nullptr || ! hasActiveSubMenu (childComp->item))
|
|
return false;
|
|
|
|
activeSubMenu.reset (new HelperClasses::MenuWindow (*(childComp->item.subMenu), this,
|
|
options.forSubmenu()
|
|
.withTargetScreenArea (childComp->getScreenBounds())
|
|
.withMinimumWidth (0),
|
|
false, managerOfChosenCommand, scaleFactor));
|
|
|
|
activeSubMenu->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
|
|
activeSubMenu->enterModalState (false);
|
|
activeSubMenu->toFront (false);
|
|
return true;
|
|
}
|
|
|
|
void triggerCurrentlyHighlightedItem()
|
|
{
|
|
if (currentChild != nullptr && canBeTriggered (currentChild->item))
|
|
dismissMenu (¤tChild->item);
|
|
}
|
|
|
|
enum class MenuSelectionDirection
|
|
{
|
|
forwards,
|
|
backwards,
|
|
current
|
|
};
|
|
|
|
void selectNextItem (MenuSelectionDirection direction)
|
|
{
|
|
disableTimerUntilMouseMoves();
|
|
|
|
auto start = [&]
|
|
{
|
|
auto index = items.indexOf (currentChild);
|
|
|
|
if (index >= 0)
|
|
return index;
|
|
|
|
return direction == MenuSelectionDirection::backwards ? items.size() - 1
|
|
: 0;
|
|
}();
|
|
|
|
auto preIncrement = (direction != MenuSelectionDirection::current && currentChild != nullptr);
|
|
|
|
for (int i = items.size(); --i >= 0;)
|
|
{
|
|
if (preIncrement)
|
|
start += (direction == MenuSelectionDirection::backwards ? -1 : 1);
|
|
|
|
if (auto* mic = items.getUnchecked ((start + items.size()) % items.size()))
|
|
{
|
|
if (canBeTriggered (mic->item) || hasActiveSubMenu (mic->item))
|
|
{
|
|
setCurrentlyHighlightedChild (mic);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (! preIncrement)
|
|
preIncrement = true;
|
|
}
|
|
}
|
|
|
|
void disableTimerUntilMouseMoves()
|
|
{
|
|
disableMouseMoves = true;
|
|
|
|
if (parent != nullptr)
|
|
parent->disableTimerUntilMouseMoves();
|
|
}
|
|
|
|
bool canScroll() const noexcept { return childYOffset != 0 || needsToScroll; }
|
|
bool isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; }
|
|
bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); }
|
|
|
|
//==============================================================================
|
|
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
|
|
{
|
|
return std::make_unique<AccessibilityHandler> (*this,
|
|
AccessibilityRole::popupMenu,
|
|
AccessibilityActions().addAction (AccessibilityActionType::focus, [this]
|
|
{
|
|
if (currentChild != nullptr)
|
|
{
|
|
if (auto* handler = currentChild->getAccessibilityHandler())
|
|
handler->grabFocus();
|
|
}
|
|
else
|
|
{
|
|
selectNextItem (MenuSelectionDirection::forwards);
|
|
}
|
|
}));
|
|
}
|
|
|
|
LookAndFeel* findLookAndFeel (const PopupMenu& menu, MenuWindow* parentWindow) const
|
|
{
|
|
return parentWindow != nullptr ? &(parentWindow->getLookAndFeel())
|
|
: menu.lookAndFeel.get();
|
|
}
|
|
|
|
LookAndFeel& findNonNullLookAndFeel (const PopupMenu& menu, MenuWindow* parentWindow) const
|
|
{
|
|
if (auto* result = findLookAndFeel (menu, parentWindow))
|
|
return *result;
|
|
|
|
return getLookAndFeel();
|
|
}
|
|
|
|
bool mouseHasBeenOver() const { return mouseWasOver; }
|
|
bool allowMouseUpToTriggerItem() const { return mouseUpCanTrigger; }
|
|
|
|
//==============================================================================
|
|
MenuWindow* parent;
|
|
const Options options;
|
|
OwnedArray<ItemComponent> items;
|
|
ApplicationCommandManager** managerOfChosenCommand;
|
|
WeakReference<Component> componentAttachedTo;
|
|
Rectangle<int> windowPos;
|
|
bool needsToScroll = false;
|
|
bool hideOnExit = false, disableMouseMoves = false, hasAnyJuceCompHadFocus = false;
|
|
int numColumns = 0, contentHeight = 0, childYOffset = 0;
|
|
Component::SafePointer<ItemComponent> currentChild;
|
|
std::unique_ptr<MenuWindow> activeSubMenu;
|
|
Array<int> columnWidths;
|
|
uint32 windowCreationTime, lastFocusedTime, timeEnteredCurrentChildComp;
|
|
OwnedArray<MouseSourceState> mouseSourceStates;
|
|
float scaleFactor;
|
|
bool exitingModalState = false;
|
|
|
|
private:
|
|
void handleMouseEvent (const MouseEvent& e)
|
|
{
|
|
mouseWasOver |= reallyContains (getLocalPoint (nullptr, e.getScreenPosition()), true);
|
|
getMouseState (e.source).handleMouseEvent (e);
|
|
}
|
|
|
|
bool mouseWasOver = false;
|
|
bool mouseUpCanTrigger = ! ModifierKeys::currentModifiers.isAnyMouseButtonDown();
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow)
|
|
};
|
|
|
|
//==============================================================================
|
|
class MouseSourceState final : public Timer
|
|
{
|
|
public:
|
|
MouseSourceState (MenuWindow& w, MouseInputSource s)
|
|
: window (w), source (s), lastScrollTime (Time::getMillisecondCounter())
|
|
{
|
|
startTimerHz (20);
|
|
}
|
|
|
|
void handleMouseEvent (const MouseEvent& e)
|
|
{
|
|
if (! window.windowIsStillValid())
|
|
return;
|
|
|
|
startTimerHz (20);
|
|
handleMousePosition (e.getScreenPosition());
|
|
}
|
|
|
|
void timerCallback() override
|
|
{
|
|
#if JUCE_WINDOWS
|
|
// touch and pen devices on Windows send an offscreen mouse move after mouse up events
|
|
// but we don't want to forward these on as they will dismiss the menu
|
|
if ((source.isTouch() || source.isPen()) && ! isValidMousePosition())
|
|
return;
|
|
#endif
|
|
|
|
if (window.windowIsStillValid())
|
|
handleMousePosition (source.getScreenPosition().roundToInt());
|
|
}
|
|
|
|
bool isOver() const
|
|
{
|
|
return window.reallyContains (window.getLocalPoint (nullptr, source.getScreenPosition()).roundToInt(), true);
|
|
}
|
|
|
|
MenuWindow& window;
|
|
MouseInputSource source;
|
|
|
|
private:
|
|
Point<int> lastMousePos;
|
|
double scrollAcceleration = 0;
|
|
uint32 lastScrollTime, lastMouseMoveTime = 0;
|
|
bool isDown = false;
|
|
|
|
void handleMousePosition (Point<int> globalMousePos)
|
|
{
|
|
auto localMousePos = window.getLocalPoint (nullptr, globalMousePos);
|
|
auto timeNow = Time::getMillisecondCounter();
|
|
|
|
if (timeNow > window.timeEnteredCurrentChildComp + 100
|
|
&& window.reallyContains (localMousePos, true)
|
|
&& window.currentChild != nullptr
|
|
&& ! (window.disableMouseMoves || window.isSubMenuVisible()))
|
|
{
|
|
window.showSubMenuFor (window.currentChild);
|
|
}
|
|
|
|
highlightItemUnderMouse (globalMousePos, localMousePos, timeNow);
|
|
|
|
const bool overScrollArea = scrollIfNecessary (localMousePos, timeNow);
|
|
const bool isOverAny = window.isOverAnyMenu();
|
|
|
|
if (window.hideOnExit && window.mouseHasBeenOver() && ! isOverAny)
|
|
window.hide (nullptr, true);
|
|
else
|
|
checkButtonState (localMousePos, timeNow, isDown, overScrollArea, isOverAny);
|
|
}
|
|
|
|
void checkButtonState (Point<int> localMousePos, const uint32 timeNow,
|
|
const bool wasDown, const bool overScrollArea, const bool isOverAny)
|
|
{
|
|
isDown = window.mouseHasBeenOver()
|
|
&& (ModifierKeys::currentModifiers.isAnyMouseButtonDown()
|
|
|| ComponentPeer::getCurrentModifiersRealtime().isAnyMouseButtonDown());
|
|
|
|
const auto reallyContained = window.reallyContains (localMousePos, true);
|
|
|
|
if (! window.doesAnyJuceCompHaveFocus() && ! reallyContained)
|
|
{
|
|
if (timeNow > window.lastFocusedTime + 10)
|
|
{
|
|
PopupMenuSettings::menuWasHiddenBecauseOfAppChange = true;
|
|
window.dismissMenu (nullptr);
|
|
// Note: This object may have been deleted by the previous call.
|
|
}
|
|
}
|
|
else if (wasDown && timeNow > window.windowCreationTime + 250 && ! isDown && ! overScrollArea)
|
|
{
|
|
if (reallyContained && window.allowMouseUpToTriggerItem())
|
|
window.triggerCurrentlyHighlightedItem();
|
|
else if ((window.mouseHasBeenOver() || ! window.allowMouseUpToTriggerItem()) && ! isOverAny)
|
|
window.dismissMenu (nullptr);
|
|
|
|
// Note: This object may have been deleted by the previous call.
|
|
}
|
|
else
|
|
{
|
|
window.lastFocusedTime = timeNow;
|
|
}
|
|
}
|
|
|
|
void highlightItemUnderMouse (Point<int> globalMousePos, Point<int> localMousePos, const uint32 timeNow)
|
|
{
|
|
if (globalMousePos != lastMousePos || timeNow > lastMouseMoveTime + 350)
|
|
{
|
|
const auto isMouseOver = window.reallyContains (localMousePos, true);
|
|
|
|
if (lastMousePos.getDistanceFrom (globalMousePos) > 2)
|
|
{
|
|
lastMouseMoveTime = timeNow;
|
|
|
|
if (window.disableMouseMoves && isMouseOver)
|
|
window.disableMouseMoves = false;
|
|
}
|
|
|
|
if (window.disableMouseMoves || (window.activeSubMenu != nullptr && window.activeSubMenu->isOverChildren()))
|
|
return;
|
|
|
|
const bool isMovingTowardsMenu = isMouseOver && globalMousePos != lastMousePos
|
|
&& isMovingTowardsSubmenu (globalMousePos);
|
|
|
|
lastMousePos = globalMousePos;
|
|
|
|
if (! isMovingTowardsMenu)
|
|
{
|
|
auto* c = window.getComponentAt (localMousePos);
|
|
|
|
if (c == &window)
|
|
c = nullptr;
|
|
|
|
auto* itemUnderMouse = dynamic_cast<ItemComponent*> (c);
|
|
|
|
if (itemUnderMouse == nullptr && c != nullptr)
|
|
itemUnderMouse = c->findParentComponentOfClass<ItemComponent>();
|
|
|
|
if (itemUnderMouse != window.currentChild
|
|
&& (isMouseOver || (window.activeSubMenu == nullptr) || ! window.activeSubMenu->isVisible()))
|
|
{
|
|
if (isMouseOver && (c != nullptr) && (window.activeSubMenu != nullptr))
|
|
window.activeSubMenu->hide (nullptr, true);
|
|
|
|
if (! isMouseOver)
|
|
{
|
|
if (! window.mouseHasBeenOver())
|
|
return;
|
|
|
|
itemUnderMouse = nullptr;
|
|
}
|
|
|
|
window.setCurrentlyHighlightedChild (itemUnderMouse);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isMovingTowardsSubmenu (Point<int> newGlobalPos) const
|
|
{
|
|
if (window.activeSubMenu == nullptr)
|
|
return false;
|
|
|
|
// try to intelligently guess whether the user is moving the mouse towards a currently-open
|
|
// submenu. To do this, look at whether the mouse stays inside a triangular region that
|
|
// extends from the last mouse pos to the submenu's rectangle..
|
|
|
|
auto itemScreenBounds = window.activeSubMenu->getScreenBounds();
|
|
auto subX = (float) itemScreenBounds.getX();
|
|
|
|
auto oldGlobalPos = lastMousePos;
|
|
|
|
if (itemScreenBounds.getX() > window.getX())
|
|
{
|
|
oldGlobalPos -= Point<int> (2, 0); // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels
|
|
}
|
|
else
|
|
{
|
|
oldGlobalPos += Point<int> (2, 0);
|
|
subX += (float) itemScreenBounds.getWidth();
|
|
}
|
|
|
|
Path areaTowardsSubMenu;
|
|
areaTowardsSubMenu.addTriangle ((float) oldGlobalPos.x, (float) oldGlobalPos.y,
|
|
subX, (float) itemScreenBounds.getY(),
|
|
subX, (float) itemScreenBounds.getBottom());
|
|
|
|
return areaTowardsSubMenu.contains (newGlobalPos.toFloat());
|
|
}
|
|
|
|
bool scrollIfNecessary (Point<int> localMousePos, const uint32 timeNow)
|
|
{
|
|
if (window.canScroll()
|
|
&& isPositiveAndBelow (localMousePos.x, window.getWidth())
|
|
&& (isPositiveAndBelow (localMousePos.y, window.getHeight()) || source.isDragging()))
|
|
{
|
|
if (window.isTopScrollZoneActive() && localMousePos.y < PopupMenuSettings::scrollZone)
|
|
return scroll (timeNow, -1);
|
|
|
|
if (window.isBottomScrollZoneActive() && localMousePos.y > window.getHeight() - PopupMenuSettings::scrollZone)
|
|
return scroll (timeNow, 1);
|
|
}
|
|
|
|
scrollAcceleration = 1.0;
|
|
return false;
|
|
}
|
|
|
|
bool scroll (const uint32 timeNow, const int direction)
|
|
{
|
|
if (timeNow > lastScrollTime + 20)
|
|
{
|
|
scrollAcceleration = jmin (4.0, scrollAcceleration * 1.04);
|
|
int amount = 0;
|
|
|
|
for (int i = 0; i < window.items.size() && amount == 0; ++i)
|
|
amount = ((int) scrollAcceleration) * window.items.getUnchecked (i)->getHeight();
|
|
|
|
window.alterChildYPos (amount * direction);
|
|
lastScrollTime = timeNow;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#if JUCE_WINDOWS
|
|
bool isValidMousePosition()
|
|
{
|
|
auto screenPos = source.getScreenPosition();
|
|
auto localPos = (window.activeSubMenu == nullptr) ? window.getLocalPoint (nullptr, screenPos)
|
|
: window.activeSubMenu->getLocalPoint (nullptr, screenPos);
|
|
|
|
if (localPos.x < 0 && localPos.y < 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MouseSourceState)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct NormalComponentWrapper final : public PopupMenu::CustomComponent
|
|
{
|
|
NormalComponentWrapper (Component& comp, int w, int h, bool triggerMenuItemAutomaticallyWhenClicked)
|
|
: PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked),
|
|
width (w), height (h)
|
|
{
|
|
addAndMakeVisible (comp);
|
|
}
|
|
|
|
void getIdealSize (int& idealWidth, int& idealHeight) override
|
|
{
|
|
idealWidth = width;
|
|
idealHeight = height;
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
if (auto* child = getChildComponent (0))
|
|
child->setBounds (getLocalBounds());
|
|
}
|
|
|
|
const int width, height;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper)
|
|
};
|
|
|
|
};
|
|
|
|
//==============================================================================
|
|
PopupMenu::PopupMenu (const PopupMenu& other)
|
|
: items (other.items),
|
|
lookAndFeel (other.lookAndFeel)
|
|
{
|
|
}
|
|
|
|
PopupMenu& PopupMenu::operator= (const PopupMenu& other)
|
|
{
|
|
if (this != &other)
|
|
{
|
|
items = other.items;
|
|
lookAndFeel = other.lookAndFeel;
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::PopupMenu (PopupMenu&& other) noexcept
|
|
: items (std::move (other.items)),
|
|
lookAndFeel (std::move (other.lookAndFeel))
|
|
{
|
|
}
|
|
|
|
PopupMenu& PopupMenu::operator= (PopupMenu&& other) noexcept
|
|
{
|
|
items = std::move (other.items);
|
|
lookAndFeel = other.lookAndFeel;
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::~PopupMenu() = default;
|
|
|
|
void PopupMenu::clear()
|
|
{
|
|
items.clear();
|
|
}
|
|
|
|
//==============================================================================
|
|
PopupMenu::Item::Item() = default;
|
|
PopupMenu::Item::Item (String t) : text (std::move (t)), itemID (-1) {}
|
|
|
|
PopupMenu::Item::Item (Item&&) = default;
|
|
PopupMenu::Item& PopupMenu::Item::operator= (Item&&) = default;
|
|
|
|
PopupMenu::Item::Item (const Item& other)
|
|
: text (other.text),
|
|
itemID (other.itemID),
|
|
action (other.action),
|
|
subMenu (createCopyIfNotNull (other.subMenu.get())),
|
|
image (other.image != nullptr ? other.image->createCopy() : nullptr),
|
|
customComponent (other.customComponent),
|
|
customCallback (other.customCallback),
|
|
commandManager (other.commandManager),
|
|
shortcutKeyDescription (other.shortcutKeyDescription),
|
|
colour (other.colour),
|
|
isEnabled (other.isEnabled),
|
|
isTicked (other.isTicked),
|
|
isSeparator (other.isSeparator),
|
|
isSectionHeader (other.isSectionHeader),
|
|
shouldBreakAfter (other.shouldBreakAfter)
|
|
{}
|
|
|
|
PopupMenu::Item& PopupMenu::Item::operator= (const Item& other)
|
|
{
|
|
text = other.text;
|
|
itemID = other.itemID;
|
|
action = other.action;
|
|
subMenu.reset (createCopyIfNotNull (other.subMenu.get()));
|
|
image = other.image != nullptr ? other.image->createCopy() : std::unique_ptr<Drawable>();
|
|
customComponent = other.customComponent;
|
|
customCallback = other.customCallback;
|
|
commandManager = other.commandManager;
|
|
shortcutKeyDescription = other.shortcutKeyDescription;
|
|
colour = other.colour;
|
|
isEnabled = other.isEnabled;
|
|
isTicked = other.isTicked;
|
|
isSeparator = other.isSeparator;
|
|
isSectionHeader = other.isSectionHeader;
|
|
shouldBreakAfter = other.shouldBreakAfter;
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::Item& PopupMenu::Item::setTicked (bool shouldBeTicked) & noexcept
|
|
{
|
|
isTicked = shouldBeTicked;
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::Item& PopupMenu::Item::setEnabled (bool shouldBeEnabled) & noexcept
|
|
{
|
|
isEnabled = shouldBeEnabled;
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::Item& PopupMenu::Item::setAction (std::function<void()> newAction) & noexcept
|
|
{
|
|
action = std::move (newAction);
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::Item& PopupMenu::Item::setID (int newID) & noexcept
|
|
{
|
|
itemID = newID;
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::Item& PopupMenu::Item::setColour (Colour newColour) & noexcept
|
|
{
|
|
colour = newColour;
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::Item& PopupMenu::Item::setCustomComponent (ReferenceCountedObjectPtr<CustomComponent> comp) & noexcept
|
|
{
|
|
customComponent = comp;
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::Item& PopupMenu::Item::setImage (std::unique_ptr<Drawable> newImage) & noexcept
|
|
{
|
|
image = std::move (newImage);
|
|
return *this;
|
|
}
|
|
|
|
PopupMenu::Item&& PopupMenu::Item::setTicked (bool shouldBeTicked) && noexcept
|
|
{
|
|
isTicked = shouldBeTicked;
|
|
return std::move (*this);
|
|
}
|
|
|
|
PopupMenu::Item&& PopupMenu::Item::setEnabled (bool shouldBeEnabled) && noexcept
|
|
{
|
|
isEnabled = shouldBeEnabled;
|
|
return std::move (*this);
|
|
}
|
|
|
|
PopupMenu::Item&& PopupMenu::Item::setAction (std::function<void()> newAction) && noexcept
|
|
{
|
|
action = std::move (newAction);
|
|
return std::move (*this);
|
|
}
|
|
|
|
PopupMenu::Item&& PopupMenu::Item::setID (int newID) && noexcept
|
|
{
|
|
itemID = newID;
|
|
return std::move (*this);
|
|
}
|
|
|
|
PopupMenu::Item&& PopupMenu::Item::setColour (Colour newColour) && noexcept
|
|
{
|
|
colour = newColour;
|
|
return std::move (*this);
|
|
}
|
|
|
|
PopupMenu::Item&& PopupMenu::Item::setCustomComponent (ReferenceCountedObjectPtr<CustomComponent> comp) && noexcept
|
|
{
|
|
customComponent = comp;
|
|
return std::move (*this);
|
|
}
|
|
|
|
PopupMenu::Item&& PopupMenu::Item::setImage (std::unique_ptr<Drawable> newImage) && noexcept
|
|
{
|
|
image = std::move (newImage);
|
|
return std::move (*this);
|
|
}
|
|
|
|
void PopupMenu::addItem (Item newItem)
|
|
{
|
|
// An ID of 0 is used as a return value to indicate that the user
|
|
// didn't pick anything, so you shouldn't use it as the ID for an item.
|
|
jassert (newItem.itemID != 0
|
|
|| newItem.isSeparator || newItem.isSectionHeader
|
|
|| newItem.subMenu != nullptr);
|
|
|
|
items.add (std::move (newItem));
|
|
}
|
|
|
|
void PopupMenu::addItem (String itemText, std::function<void()> action)
|
|
{
|
|
addItem (std::move (itemText), true, false, std::move (action));
|
|
}
|
|
|
|
void PopupMenu::addItem (String itemText, bool isActive, bool isTicked, std::function<void()> action)
|
|
{
|
|
Item i (std::move (itemText));
|
|
i.action = std::move (action);
|
|
i.isEnabled = isActive;
|
|
i.isTicked = isTicked;
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
void PopupMenu::addItem (int itemResultID, String itemText, bool isActive, bool isTicked)
|
|
{
|
|
Item i (std::move (itemText));
|
|
i.itemID = itemResultID;
|
|
i.isEnabled = isActive;
|
|
i.isTicked = isTicked;
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
static std::unique_ptr<Drawable> createDrawableFromImage (const Image& im)
|
|
{
|
|
if (im.isValid())
|
|
{
|
|
auto d = new DrawableImage();
|
|
d->setImage (im);
|
|
return std::unique_ptr<Drawable> (d);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void PopupMenu::addItem (int itemResultID, String itemText, bool isActive, bool isTicked, const Image& iconToUse)
|
|
{
|
|
addItem (itemResultID, std::move (itemText), isActive, isTicked, createDrawableFromImage (iconToUse));
|
|
}
|
|
|
|
void PopupMenu::addItem (int itemResultID, String itemText, bool isActive,
|
|
bool isTicked, std::unique_ptr<Drawable> iconToUse)
|
|
{
|
|
Item i (std::move (itemText));
|
|
i.itemID = itemResultID;
|
|
i.isEnabled = isActive;
|
|
i.isTicked = isTicked;
|
|
i.image = std::move (iconToUse);
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager,
|
|
const CommandID commandID,
|
|
String displayName,
|
|
std::unique_ptr<Drawable> iconToUse)
|
|
{
|
|
jassert (commandManager != nullptr && commandID != 0);
|
|
|
|
if (auto* registeredInfo = commandManager->getCommandForID (commandID))
|
|
{
|
|
ApplicationCommandInfo info (*registeredInfo);
|
|
auto* target = commandManager->getTargetForCommand (commandID, info);
|
|
|
|
Item i;
|
|
i.text = displayName.isNotEmpty() ? std::move (displayName) : info.shortName;
|
|
i.itemID = (int) commandID;
|
|
i.commandManager = commandManager;
|
|
i.isEnabled = target != nullptr && (info.flags & ApplicationCommandInfo::isDisabled) == 0;
|
|
i.isTicked = (info.flags & ApplicationCommandInfo::isTicked) != 0;
|
|
i.image = std::move (iconToUse);
|
|
addItem (std::move (i));
|
|
}
|
|
}
|
|
|
|
void PopupMenu::addColouredItem (int itemResultID, String itemText, Colour itemTextColour,
|
|
bool isActive, bool isTicked, std::unique_ptr<Drawable> iconToUse)
|
|
{
|
|
Item i (std::move (itemText));
|
|
i.itemID = itemResultID;
|
|
i.colour = itemTextColour;
|
|
i.isEnabled = isActive;
|
|
i.isTicked = isTicked;
|
|
i.image = std::move (iconToUse);
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
void PopupMenu::addColouredItem (int itemResultID, String itemText, Colour itemTextColour,
|
|
bool isActive, bool isTicked, const Image& iconToUse)
|
|
{
|
|
Item i (std::move (itemText));
|
|
i.itemID = itemResultID;
|
|
i.colour = itemTextColour;
|
|
i.isEnabled = isActive;
|
|
i.isTicked = isTicked;
|
|
i.image = createDrawableFromImage (iconToUse);
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
void PopupMenu::addCustomItem (int itemResultID,
|
|
std::unique_ptr<CustomComponent> cc,
|
|
std::unique_ptr<const PopupMenu> subMenu,
|
|
const String& itemTitle)
|
|
{
|
|
Item i;
|
|
i.text = itemTitle;
|
|
i.itemID = itemResultID;
|
|
i.customComponent = cc.release();
|
|
i.subMenu.reset (createCopyIfNotNull (subMenu.get()));
|
|
|
|
// If this assertion is hit, this item will be visible to screen readers but with
|
|
// no name, which may be confusing to users.
|
|
// It's probably a good idea to add a title for this menu item that describes
|
|
// the meaning of the item, or the contents of the submenu, as appropriate.
|
|
// If you don't want this menu item to be press-able directly, pass "false" to the
|
|
// constructor of the CustomComponent.
|
|
jassert (! (HelperClasses::ItemComponent::isAccessibilityHandlerRequired (i) && itemTitle.isEmpty()));
|
|
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
void PopupMenu::addCustomItem (int itemResultID,
|
|
Component& customComponent,
|
|
int idealWidth, int idealHeight,
|
|
bool triggerMenuItemAutomaticallyWhenClicked,
|
|
std::unique_ptr<const PopupMenu> subMenu,
|
|
const String& itemTitle)
|
|
{
|
|
auto comp = std::make_unique<HelperClasses::NormalComponentWrapper> (customComponent, idealWidth, idealHeight,
|
|
triggerMenuItemAutomaticallyWhenClicked);
|
|
addCustomItem (itemResultID, std::move (comp), std::move (subMenu), itemTitle);
|
|
}
|
|
|
|
void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive)
|
|
{
|
|
addSubMenu (std::move (subMenuName), std::move (subMenu), isActive, nullptr, false, 0);
|
|
}
|
|
|
|
void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive,
|
|
const Image& iconToUse, bool isTicked, int itemResultID)
|
|
{
|
|
addSubMenu (std::move (subMenuName), std::move (subMenu), isActive,
|
|
createDrawableFromImage (iconToUse), isTicked, itemResultID);
|
|
}
|
|
|
|
void PopupMenu::addSubMenu (String subMenuName, PopupMenu subMenu, bool isActive,
|
|
std::unique_ptr<Drawable> iconToUse, bool isTicked, int itemResultID)
|
|
{
|
|
Item i (std::move (subMenuName));
|
|
i.itemID = itemResultID;
|
|
i.isEnabled = isActive && (itemResultID != 0 || subMenu.getNumItems() > 0);
|
|
i.subMenu.reset (new PopupMenu (std::move (subMenu)));
|
|
i.isTicked = isTicked;
|
|
i.image = std::move (iconToUse);
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
void PopupMenu::addSeparator()
|
|
{
|
|
if (items.size() > 0 && ! items.getLast().isSeparator)
|
|
{
|
|
Item i;
|
|
i.isSeparator = true;
|
|
addItem (std::move (i));
|
|
}
|
|
}
|
|
|
|
void PopupMenu::addSectionHeader (String title)
|
|
{
|
|
Item i (std::move (title));
|
|
i.itemID = 0;
|
|
i.isSectionHeader = true;
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
void PopupMenu::addColumnBreak()
|
|
{
|
|
if (! items.isEmpty())
|
|
std::prev (items.end())->shouldBreakAfter = true;
|
|
}
|
|
|
|
//==============================================================================
|
|
PopupMenu::Options::Options()
|
|
{
|
|
targetArea.setPosition (Desktop::getMousePosition());
|
|
}
|
|
|
|
template <typename Member, typename Item>
|
|
static PopupMenu::Options with (PopupMenu::Options options, Member&& member, Item&& item)
|
|
{
|
|
options.*member = std::forward<Item> (item);
|
|
return options;
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withTargetComponent (Component* comp) const
|
|
{
|
|
auto o = with (with (*this, &Options::targetComponent, comp), &Options::topLevelTarget, comp);
|
|
|
|
if (comp != nullptr)
|
|
o.targetArea = comp->getScreenBounds();
|
|
|
|
return o;
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withTargetComponent (Component& comp) const
|
|
{
|
|
return withTargetComponent (&comp);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withTargetScreenArea (Rectangle<int> area) const
|
|
{
|
|
return with (*this, &Options::targetArea, area);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withMousePosition() const
|
|
{
|
|
return withTargetScreenArea (Rectangle<int>{}.withPosition (Desktop::getMousePosition()));
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withDeletionCheck (Component& comp) const
|
|
{
|
|
return with (with (*this, &Options::isWatchingForDeletion, true),
|
|
&Options::componentToWatchForDeletion,
|
|
&comp);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withMinimumWidth (int w) const
|
|
{
|
|
return with (*this, &Options::minWidth, w);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withMinimumNumColumns (int cols) const
|
|
{
|
|
return with (*this, &Options::minColumns, cols);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withMaximumNumColumns (int cols) const
|
|
{
|
|
return with (*this, &Options::maxColumns, cols);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withStandardItemHeight (int height) const
|
|
{
|
|
return with (*this, &Options::standardHeight, height);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withItemThatMustBeVisible (int idOfItemToBeVisible) const
|
|
{
|
|
return with (*this, &Options::visibleItemID, idOfItemToBeVisible);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withParentComponent (Component* parent) const
|
|
{
|
|
return with (*this, &Options::parentComponent, parent);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withPreferredPopupDirection (PopupDirection direction) const
|
|
{
|
|
return with (*this, &Options::preferredPopupDirection, direction);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withInitiallySelectedItem (int idOfItemToBeSelected) const
|
|
{
|
|
return with (*this, &Options::initiallySelectedItemId, idOfItemToBeSelected);
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::forSubmenu() const
|
|
{
|
|
return with (*this, &Options::targetComponent, nullptr);
|
|
}
|
|
|
|
Component* PopupMenu::createWindow (const Options& options,
|
|
ApplicationCommandManager** managerOfChosenCommand) const
|
|
{
|
|
#if JUCE_WINDOWS
|
|
const auto scope = [&]() -> std::unique_ptr<ScopedThreadDPIAwarenessSetter>
|
|
{
|
|
if (auto* target = options.getTargetComponent())
|
|
if (auto* handle = target->getWindowHandle())
|
|
return std::make_unique<ScopedThreadDPIAwarenessSetter> (handle);
|
|
|
|
return nullptr;
|
|
}();
|
|
#endif
|
|
|
|
return items.isEmpty() ? nullptr
|
|
: new HelperClasses::MenuWindow (*this, nullptr, options,
|
|
! options.getTargetScreenArea().isEmpty(),
|
|
managerOfChosenCommand);
|
|
}
|
|
|
|
//==============================================================================
|
|
// This invokes any command manager commands and deletes the menu window when it is dismissed
|
|
struct PopupMenuCompletionCallback final : public ModalComponentManager::Callback
|
|
{
|
|
PopupMenuCompletionCallback() = default;
|
|
|
|
void modalStateFinished (int result) override
|
|
{
|
|
if (managerOfChosenCommand != nullptr && result != 0)
|
|
{
|
|
ApplicationCommandTarget::InvocationInfo info (result);
|
|
info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
|
|
|
|
managerOfChosenCommand->invoke (info, true);
|
|
}
|
|
|
|
// (this would be the place to fade out the component, if that's what's required)
|
|
component.reset();
|
|
|
|
if (PopupMenuSettings::menuWasHiddenBecauseOfAppChange)
|
|
return;
|
|
|
|
if (auto* focusComponent = Component::getCurrentlyFocusedComponent())
|
|
{
|
|
const auto focusedIsNotMinimised = [focusComponent]
|
|
{
|
|
if (auto* peer = focusComponent->getPeer())
|
|
return ! peer->isMinimised();
|
|
|
|
return false;
|
|
}();
|
|
|
|
if (focusedIsNotMinimised)
|
|
{
|
|
if (auto* topLevel = focusComponent->getTopLevelComponent())
|
|
topLevel->toFront (true);
|
|
|
|
if (focusComponent->isShowing() && ! focusComponent->hasKeyboardFocus (true))
|
|
focusComponent->grabKeyboardFocus();
|
|
}
|
|
}
|
|
}
|
|
|
|
ApplicationCommandManager* managerOfChosenCommand = nullptr;
|
|
std::unique_ptr<Component> component;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback)
|
|
};
|
|
|
|
int PopupMenu::showWithOptionalCallback (const Options& options,
|
|
ModalComponentManager::Callback* userCallback,
|
|
[[maybe_unused]] bool canBeModal)
|
|
{
|
|
std::unique_ptr<ModalComponentManager::Callback> userCallbackDeleter (userCallback);
|
|
std::unique_ptr<PopupMenuCompletionCallback> callback (new PopupMenuCompletionCallback());
|
|
|
|
if (auto* window = createWindow (options, &(callback->managerOfChosenCommand)))
|
|
{
|
|
callback->component.reset (window);
|
|
|
|
PopupMenuSettings::menuWasHiddenBecauseOfAppChange = false;
|
|
|
|
window->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
|
|
window->enterModalState (false, userCallbackDeleter.release());
|
|
ModalComponentManager::getInstance()->attachCallback (window, callback.release());
|
|
|
|
window->toFront (false); // need to do this after making it modal, or it could
|
|
// be stuck behind other comps that are already modal..
|
|
|
|
#if JUCE_MODAL_LOOPS_PERMITTED
|
|
if (userCallback == nullptr && canBeModal)
|
|
return window->runModalLoop();
|
|
#else
|
|
jassert (! (userCallback == nullptr && canBeModal));
|
|
#endif
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//==============================================================================
|
|
#if JUCE_MODAL_LOOPS_PERMITTED
|
|
int PopupMenu::showMenu (const Options& options)
|
|
{
|
|
return showWithOptionalCallback (options, nullptr, true);
|
|
}
|
|
#endif
|
|
|
|
void PopupMenu::showMenuAsync (const Options& options)
|
|
{
|
|
showWithOptionalCallback (options, nullptr, false);
|
|
}
|
|
|
|
void PopupMenu::showMenuAsync (const Options& options, ModalComponentManager::Callback* userCallback)
|
|
{
|
|
#if ! JUCE_MODAL_LOOPS_PERMITTED
|
|
jassert (userCallback != nullptr);
|
|
#endif
|
|
|
|
showWithOptionalCallback (options, userCallback, false);
|
|
}
|
|
|
|
void PopupMenu::showMenuAsync (const Options& options, std::function<void (int)> userCallback)
|
|
{
|
|
showWithOptionalCallback (options, ModalCallbackFunction::create (userCallback), false);
|
|
}
|
|
|
|
//==============================================================================
|
|
#if JUCE_MODAL_LOOPS_PERMITTED
|
|
int PopupMenu::show (int itemIDThatMustBeVisible, int minimumWidth,
|
|
int maximumNumColumns, int standardItemHeight,
|
|
ModalComponentManager::Callback* callback)
|
|
{
|
|
return showWithOptionalCallback (Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
|
|
.withMinimumWidth (minimumWidth)
|
|
.withMaximumNumColumns (maximumNumColumns)
|
|
.withStandardItemHeight (standardItemHeight),
|
|
callback, true);
|
|
}
|
|
|
|
int PopupMenu::showAt (Rectangle<int> screenAreaToAttachTo,
|
|
int itemIDThatMustBeVisible, int minimumWidth,
|
|
int maximumNumColumns, int standardItemHeight,
|
|
ModalComponentManager::Callback* callback)
|
|
{
|
|
return showWithOptionalCallback (Options().withTargetScreenArea (screenAreaToAttachTo)
|
|
.withItemThatMustBeVisible (itemIDThatMustBeVisible)
|
|
.withMinimumWidth (minimumWidth)
|
|
.withMaximumNumColumns (maximumNumColumns)
|
|
.withStandardItemHeight (standardItemHeight),
|
|
callback, true);
|
|
}
|
|
|
|
int PopupMenu::showAt (Component* componentToAttachTo,
|
|
int itemIDThatMustBeVisible, int minimumWidth,
|
|
int maximumNumColumns, int standardItemHeight,
|
|
ModalComponentManager::Callback* callback)
|
|
{
|
|
auto options = Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
|
|
.withMinimumWidth (minimumWidth)
|
|
.withMaximumNumColumns (maximumNumColumns)
|
|
.withStandardItemHeight (standardItemHeight);
|
|
|
|
if (componentToAttachTo != nullptr)
|
|
options = options.withTargetComponent (componentToAttachTo);
|
|
|
|
return showWithOptionalCallback (options, callback, true);
|
|
}
|
|
#endif
|
|
|
|
bool JUCE_CALLTYPE PopupMenu::dismissAllActiveMenus()
|
|
{
|
|
auto& windows = HelperClasses::MenuWindow::getActiveWindows();
|
|
auto numWindows = windows.size();
|
|
|
|
for (int i = numWindows; --i >= 0;)
|
|
{
|
|
if (auto* pmw = windows[i])
|
|
{
|
|
pmw->setLookAndFeel (nullptr);
|
|
pmw->dismissMenu (nullptr);
|
|
}
|
|
}
|
|
|
|
return numWindows > 0;
|
|
}
|
|
|
|
//==============================================================================
|
|
int PopupMenu::getNumItems() const noexcept
|
|
{
|
|
int num = 0;
|
|
|
|
for (auto& mi : items)
|
|
if (! mi.isSeparator)
|
|
++num;
|
|
|
|
return num;
|
|
}
|
|
|
|
bool PopupMenu::containsCommandItem (const int commandID) const
|
|
{
|
|
for (auto& mi : items)
|
|
if ((mi.itemID == commandID && mi.commandManager != nullptr)
|
|
|| (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID)))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool PopupMenu::containsAnyActiveItems() const noexcept
|
|
{
|
|
for (auto& mi : items)
|
|
{
|
|
if (mi.subMenu != nullptr)
|
|
{
|
|
if (mi.subMenu->containsAnyActiveItems())
|
|
return true;
|
|
}
|
|
else if (mi.isEnabled)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel)
|
|
{
|
|
lookAndFeel = newLookAndFeel;
|
|
}
|
|
|
|
void PopupMenu::setItem (CustomComponent& c, const Item* itemToUse)
|
|
{
|
|
c.item = itemToUse;
|
|
c.repaint();
|
|
}
|
|
|
|
//==============================================================================
|
|
PopupMenu::CustomComponent::CustomComponent() : CustomComponent (true) {}
|
|
|
|
PopupMenu::CustomComponent::CustomComponent (bool autoTrigger)
|
|
: triggeredAutomatically (autoTrigger)
|
|
{
|
|
}
|
|
|
|
void PopupMenu::CustomComponent::setHighlighted (bool shouldBeHighlighted)
|
|
{
|
|
isHighlighted = shouldBeHighlighted;
|
|
repaint();
|
|
}
|
|
|
|
void PopupMenu::CustomComponent::triggerMenuItem()
|
|
{
|
|
if (auto* mic = findParentComponentOfClass<HelperClasses::ItemComponent>())
|
|
{
|
|
if (auto* pmw = mic->findParentComponentOfClass<HelperClasses::MenuWindow>())
|
|
{
|
|
pmw->dismissMenu (&mic->item);
|
|
}
|
|
else
|
|
{
|
|
// something must have gone wrong with the component hierarchy if this happens..
|
|
jassertfalse;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// why isn't this component inside a menu? Not much point triggering the item if
|
|
// there's no menu.
|
|
jassertfalse;
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
PopupMenu::CustomCallback::CustomCallback() {}
|
|
PopupMenu::CustomCallback::~CustomCallback() {}
|
|
|
|
//==============================================================================
|
|
PopupMenu::MenuItemIterator::MenuItemIterator (const PopupMenu& m, bool recurse) : searchRecursively (recurse)
|
|
{
|
|
index.add (0);
|
|
menus.add (&m);
|
|
}
|
|
|
|
PopupMenu::MenuItemIterator::~MenuItemIterator() = default;
|
|
|
|
bool PopupMenu::MenuItemIterator::next()
|
|
{
|
|
if (index.size() == 0 || menus.getLast()->items.size() == 0)
|
|
return false;
|
|
|
|
currentItem = const_cast<PopupMenu::Item*> (&(menus.getLast()->items.getReference (index.getLast())));
|
|
|
|
if (searchRecursively && currentItem->subMenu != nullptr)
|
|
{
|
|
index.add (0);
|
|
menus.add (currentItem->subMenu.get());
|
|
}
|
|
else
|
|
{
|
|
index.setUnchecked (index.size() - 1, index.getLast() + 1);
|
|
}
|
|
|
|
while (index.size() > 0 && index.getLast() >= (int) menus.getLast()->items.size())
|
|
{
|
|
index.removeLast();
|
|
menus.removeLast();
|
|
|
|
if (index.size() > 0)
|
|
index.setUnchecked (index.size() - 1, index.getLast() + 1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
PopupMenu::Item& PopupMenu::MenuItemIterator::getItem() const
|
|
{
|
|
jassert (currentItem != nullptr);
|
|
return *(currentItem);
|
|
}
|
|
|
|
void PopupMenu::LookAndFeelMethods::drawPopupMenuBackground (Graphics&, int, int) {}
|
|
|
|
void PopupMenu::LookAndFeelMethods::drawPopupMenuItem (Graphics&, const Rectangle<int>&,
|
|
bool, bool, bool,
|
|
bool, bool,
|
|
const String&,
|
|
const String&,
|
|
const Drawable*,
|
|
const Colour*) {}
|
|
|
|
void PopupMenu::LookAndFeelMethods::drawPopupMenuSectionHeader (Graphics&, const Rectangle<int>&,
|
|
const String&) {}
|
|
|
|
void PopupMenu::LookAndFeelMethods::drawPopupMenuUpDownArrow (Graphics&, int, int, bool) {}
|
|
|
|
void PopupMenu::LookAndFeelMethods::getIdealPopupMenuItemSize (const String&, bool, int, int&, int&) {}
|
|
|
|
int PopupMenu::LookAndFeelMethods::getPopupMenuBorderSize() { return 0; }
|
|
|
|
} // namespace juce
|