25bd5d8adb
subrepo: subdir: "deps/juce" merged: "b13f9084e" upstream: origin: "https://github.com/essej/JUCE.git" branch: "sono6good" commit: "b13f9084e" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo.git" commit: "2f68596"
2328 lines
81 KiB
C++
2328 lines
81 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2020 - Raw Material Software Limited
|
|
|
|
JUCE is an open source library subject to commercial or open-source
|
|
licensing.
|
|
|
|
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
|
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
|
|
|
End User License Agreement: www.juce.com/juce-6-licence
|
|
Privacy Policy: www.juce.com/juce-privacy-policy
|
|
|
|
Or: You may also use this code under the terms of the GPL v3 (see
|
|
www.gnu.org/licenses).
|
|
|
|
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
|
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR 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; }
|
|
static bool hasActiveSubMenu (const PopupMenu::Item& item) noexcept { return item.isEnabled && item.subMenu != nullptr && item.subMenu->items.size() > 0; }
|
|
|
|
//==============================================================================
|
|
struct HeaderItemComponent : 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;
|
|
}
|
|
|
|
const Options& options;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HeaderItemComponent)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct ItemComponent : 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();
|
|
}
|
|
}
|
|
|
|
PopupMenu::Item item;
|
|
|
|
private:
|
|
//==============================================================================
|
|
class ItemAccessibilityHandler : public AccessibilityHandler
|
|
{
|
|
public:
|
|
explicit ItemAccessibilityHandler (ItemComponent& itemComponentToWrap)
|
|
: AccessibilityHandler (itemComponentToWrap,
|
|
AccessibilityRole::menuItem,
|
|
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();
|
|
}
|
|
|
|
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 onPress = [&item]
|
|
{
|
|
item.parentWindow.setCurrentlyHighlightedChild (&item);
|
|
item.parentWindow.triggerCurrentlyHighlightedItem();
|
|
};
|
|
|
|
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::press, std::move (onPress))
|
|
.addAction (AccessibilityActionType::toggle, std::move (onToggle));
|
|
|
|
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 ? nullptr : 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 : public Component
|
|
{
|
|
MenuWindow (const PopupMenu& menu, MenuWindow* parentWindow,
|
|
Options opts, bool alignToRectangle, bool shouldDismissOnMouseUp,
|
|
ApplicationCommandManager** manager, float parentScaleFactor = 1.0f)
|
|
: Component ("menu"),
|
|
parent (parentWindow),
|
|
options (opts.withParentComponent (getLookAndFeel().getParentComponentForMenuOptions (opts))),
|
|
managerOfChosenCommand (manager),
|
|
componentAttachedTo (options.getTargetComponent()),
|
|
dismissOnMouseUp (shouldDismissOnMouseUp),
|
|
windowCreationTime (Time::getMillisecondCounter()),
|
|
lastFocusedTime (windowCreationTime),
|
|
timeEnteredCurrentChildComp (windowCreationTime),
|
|
scaleFactor (parentWindow != nullptr ? parentScaleFactor : 1.0f)
|
|
{
|
|
setWantsKeyboardFocus (false);
|
|
setMouseClickGrabsKeyboardFocus (false);
|
|
setAlwaysOnTop (true);
|
|
setFocusContainerType (FocusContainerType::focusContainer);
|
|
|
|
setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel())
|
|
: menu.lookAndFeel.get());
|
|
|
|
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));
|
|
|
|
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 (makeInvisible && deletionChecker != nullptr)
|
|
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 mouseMove (const MouseEvent& e) override { handleMouseEvent (e); }
|
|
void mouseDown (const MouseEvent& e) override { handleMouseEvent (e); }
|
|
void mouseDrag (const MouseEvent& e) override { handleMouseEvent (e); }
|
|
void mouseUp (const MouseEvent& e) override { handleMouseEvent (e); }
|
|
|
|
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
|
|
{
|
|
alterChildYPos (roundToInt (-10.0f * wheel.deltaY * PopupMenuSettings::scrollZone));
|
|
}
|
|
|
|
void handleMouseEvent (const MouseEvent& e)
|
|
{
|
|
getMouseState (e.source).handleMouseEvent (e);
|
|
}
|
|
|
|
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;
|
|
|
|
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 (! 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->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))
|
|
{
|
|
activeSubMenu.reset (new HelperClasses::MenuWindow (*(childComp->item.subMenu), this,
|
|
options.withTargetScreenArea (childComp->getScreenBounds())
|
|
.withMinimumWidth (0)
|
|
.withTargetComponent (nullptr),
|
|
false, dismissOnMouseUp, managerOfChosenCommand, scaleFactor));
|
|
|
|
activeSubMenu->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion)
|
|
activeSubMenu->enterModalState (false);
|
|
activeSubMenu->toFront (false);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void triggerCurrentlyHighlightedItem()
|
|
{
|
|
if (currentChild != nullptr
|
|
&& canBeTriggered (currentChild->item)
|
|
&& (currentChild->item.customComponent == nullptr
|
|
|| currentChild->item.customComponent->isTriggeredAutomatically()))
|
|
{
|
|
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);
|
|
}
|
|
}));
|
|
}
|
|
|
|
//==============================================================================
|
|
MenuWindow* parent;
|
|
const Options options;
|
|
OwnedArray<ItemComponent> items;
|
|
ApplicationCommandManager** managerOfChosenCommand;
|
|
WeakReference<Component> componentAttachedTo;
|
|
Rectangle<int> windowPos;
|
|
bool hasBeenOver = false, needsToScroll = false;
|
|
bool dismissOnMouseUp, 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;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow)
|
|
};
|
|
|
|
//==============================================================================
|
|
class MouseSourceState : 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.hasBeenOver && ! 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.hasBeenOver
|
|
&& (ModifierKeys::currentModifiers.isAnyMouseButtonDown()
|
|
|| ComponentPeer::getCurrentModifiersRealtime().isAnyMouseButtonDown());
|
|
|
|
if (! window.doesAnyJuceCompHaveFocus())
|
|
{
|
|
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 (window.reallyContains (localMousePos, true))
|
|
window.triggerCurrentlyHighlightedItem();
|
|
else if ((window.hasBeenOver || ! window.dismissOnMouseUp) && ! 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 (isMouseOver)
|
|
window.hasBeenOver = 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.hasBeenOver)
|
|
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 : 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)
|
|
{
|
|
Item i;
|
|
i.itemID = itemResultID;
|
|
i.customComponent = cc.release();
|
|
i.subMenu.reset (createCopyIfNotNull (subMenu.get()));
|
|
addItem (std::move (i));
|
|
}
|
|
|
|
void PopupMenu::addCustomItem (int itemResultID,
|
|
Component& customComponent,
|
|
int idealWidth, int idealHeight,
|
|
bool triggerMenuItemAutomaticallyWhenClicked,
|
|
std::unique_ptr<const PopupMenu> subMenu)
|
|
{
|
|
auto comp = std::make_unique<HelperClasses::NormalComponentWrapper> (customComponent, idealWidth, idealHeight,
|
|
triggerMenuItemAutomaticallyWhenClicked);
|
|
addCustomItem (itemResultID, std::move (comp), std::move (subMenu));
|
|
}
|
|
|
|
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 (*this, &Options::targetComponent, 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);
|
|
}
|
|
|
|
Component* PopupMenu::createWindow (const Options& options,
|
|
ApplicationCommandManager** managerOfChosenCommand) const
|
|
{
|
|
return items.isEmpty() ? nullptr
|
|
: new HelperClasses::MenuWindow (*this, nullptr, options,
|
|
! options.getTargetScreenArea().isEmpty(),
|
|
ModifierKeys::currentModifiers.isAnyMouseButtonDown(),
|
|
managerOfChosenCommand);
|
|
}
|
|
|
|
//==============================================================================
|
|
// This invokes any command manager commands and deletes the menu window when it is dismissed
|
|
struct PopupMenuCompletionCallback : 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;
|
|
|
|
auto* focusComponent = getComponentToPassFocusTo();
|
|
|
|
const auto focusedIsNotMinimised = [focusComponent]
|
|
{
|
|
if (focusComponent != nullptr)
|
|
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();
|
|
}
|
|
}
|
|
|
|
Component* getComponentToPassFocusTo() const
|
|
{
|
|
if (auto* current = Component::getCurrentlyFocusedComponent())
|
|
return current;
|
|
|
|
return prevFocused.get();
|
|
}
|
|
|
|
ApplicationCommandManager* managerOfChosenCommand = nullptr;
|
|
std::unique_ptr<Component> component;
|
|
WeakReference<Component> prevFocused { Component::getCurrentlyFocusedComponent() };
|
|
|
|
JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback)
|
|
};
|
|
|
|
int PopupMenu::showWithOptionalCallback (const Options& options,
|
|
ModalComponentManager::Callback* userCallback,
|
|
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
|
|
ignoreUnused (canBeModal);
|
|
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 (bool autoTrigger)
|
|
: triggeredAutomatically (autoTrigger)
|
|
{
|
|
}
|
|
|
|
PopupMenu::CustomComponent::~CustomComponent()
|
|
{
|
|
}
|
|
|
|
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
|