mirror of https://github.com/dcoredump/dexed.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1770 lines
61 KiB
1770 lines
61 KiB
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2013 - Raw Material Software Ltd.
|
|
|
|
Permission is granted to use this software under the terms of either:
|
|
a) the GPL v2 (or any later version)
|
|
b) the Affero GPL v3
|
|
|
|
Details of these licenses can be found at: www.gnu.org/licenses
|
|
|
|
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
To release a closed-source product which uses JUCE, commercial licenses are
|
|
available: visit www.juce.com for more information.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
//==============================================================================
|
|
namespace PopupMenuSettings
|
|
{
|
|
const int scrollZone = 24;
|
|
const int borderSize = 2;
|
|
const int timerInterval = 50;
|
|
const int dismissCommandId = 0x6287345f;
|
|
const int sectionHeaderID = 0x4734a34f;
|
|
|
|
static bool menuWasHiddenBecauseOfAppChange = false;
|
|
}
|
|
|
|
class PopupMenu::Item
|
|
{
|
|
public:
|
|
Item() : itemID (0), isActive (true), isSeparator (true), isTicked (false),
|
|
usesColour (false), commandManager (nullptr)
|
|
{}
|
|
|
|
Item (const int itemId,
|
|
const String& name,
|
|
const bool active,
|
|
const bool ticked,
|
|
Drawable* drawable,
|
|
const Colour colour,
|
|
const bool useColour,
|
|
CustomComponent* const custom,
|
|
const PopupMenu* const sub,
|
|
ApplicationCommandManager* const manager)
|
|
|
|
: itemID (itemId), text (name), textColour (colour),
|
|
isActive (active), isSeparator (false), isTicked (ticked),
|
|
usesColour (useColour), iconDrawable (drawable),
|
|
customComp (custom), subMenu (createCopyIfNotNull (sub)), commandManager (manager)
|
|
{
|
|
if (commandManager != nullptr && itemID != 0)
|
|
{
|
|
String shortcutKey;
|
|
|
|
const Array <KeyPress> keyPresses (commandManager->getKeyMappings()
|
|
->getKeyPressesAssignedToCommand (itemID));
|
|
|
|
for (int i = 0; i < keyPresses.size(); ++i)
|
|
{
|
|
const String key (keyPresses.getReference(i).getTextDescriptionWithIcons());
|
|
|
|
if (shortcutKey.isNotEmpty())
|
|
shortcutKey << ", ";
|
|
|
|
if (key.length() == 1 && key[0] < 128)
|
|
shortcutKey << "shortcut: '" << key << '\'';
|
|
else
|
|
shortcutKey << key;
|
|
}
|
|
|
|
shortcutKey = shortcutKey.trim();
|
|
|
|
if (shortcutKey.isNotEmpty())
|
|
text << "<end>" << shortcutKey;
|
|
}
|
|
}
|
|
|
|
Item (const Item& other)
|
|
: itemID (other.itemID),
|
|
text (other.text),
|
|
textColour (other.textColour),
|
|
isActive (other.isActive),
|
|
isSeparator (other.isSeparator),
|
|
isTicked (other.isTicked),
|
|
usesColour (other.usesColour),
|
|
iconDrawable (other.iconDrawable != nullptr ? other.iconDrawable->createCopy() : nullptr),
|
|
customComp (other.customComp),
|
|
subMenu (createCopyIfNotNull (other.subMenu.get())),
|
|
commandManager (other.commandManager)
|
|
{}
|
|
|
|
bool canBeTriggered() const noexcept { return isActive && itemID != 0 && itemID != PopupMenuSettings::sectionHeaderID; }
|
|
bool hasActiveSubMenu() const noexcept { return isActive && subMenu != nullptr && subMenu->items.size() > 0; }
|
|
|
|
//==============================================================================
|
|
const int itemID;
|
|
String text;
|
|
const Colour textColour;
|
|
const bool isActive, isSeparator, isTicked, usesColour;
|
|
ScopedPointer<Drawable> iconDrawable;
|
|
ReferenceCountedObjectPtr <CustomComponent> customComp;
|
|
ScopedPointer <PopupMenu> subMenu;
|
|
ApplicationCommandManager* const commandManager;
|
|
|
|
private:
|
|
Item& operator= (const Item&);
|
|
|
|
JUCE_LEAK_DETECTOR (Item)
|
|
};
|
|
|
|
|
|
//==============================================================================
|
|
struct PopupMenu::HelperClasses
|
|
{
|
|
|
|
class MouseSourceState;
|
|
class MenuWindow;
|
|
|
|
//==============================================================================
|
|
class ItemComponent : public Component
|
|
{
|
|
public:
|
|
ItemComponent (const PopupMenu::Item& info, int standardItemHeight, MenuWindow& parent)
|
|
: itemInfo (info),
|
|
isHighlighted (false)
|
|
{
|
|
addAndMakeVisible (itemInfo.customComp);
|
|
parent.addAndMakeVisible (this);
|
|
|
|
int itemW = 80;
|
|
int itemH = 16;
|
|
getIdealSize (itemW, itemH, standardItemHeight);
|
|
setSize (itemW, jlimit (2, 600, itemH));
|
|
|
|
addMouseListener (&parent, false);
|
|
}
|
|
|
|
~ItemComponent()
|
|
{
|
|
removeChildComponent (itemInfo.customComp);
|
|
}
|
|
|
|
void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight)
|
|
{
|
|
if (itemInfo.customComp != nullptr)
|
|
itemInfo.customComp->getIdealSize (idealWidth, idealHeight);
|
|
else
|
|
getLookAndFeel().getIdealPopupMenuItemSize (itemInfo.text,
|
|
itemInfo.isSeparator,
|
|
standardItemHeight,
|
|
idealWidth, idealHeight);
|
|
}
|
|
|
|
void paint (Graphics& g) override
|
|
{
|
|
if (itemInfo.customComp == nullptr)
|
|
{
|
|
String mainText (itemInfo.text);
|
|
String endText;
|
|
const int endIndex = mainText.indexOf ("<end>");
|
|
|
|
if (endIndex >= 0)
|
|
{
|
|
endText = mainText.substring (endIndex + 5).trim();
|
|
mainText = mainText.substring (0, endIndex);
|
|
}
|
|
|
|
getLookAndFeel()
|
|
.drawPopupMenuItem (g, getLocalBounds(),
|
|
itemInfo.isSeparator,
|
|
itemInfo.isActive,
|
|
isHighlighted,
|
|
itemInfo.isTicked,
|
|
itemInfo.subMenu != nullptr && (itemInfo.itemID == 0 || itemInfo.subMenu->getNumItems() > 0),
|
|
mainText, endText,
|
|
itemInfo.iconDrawable,
|
|
itemInfo.usesColour ? &(itemInfo.textColour) : nullptr);
|
|
}
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
if (Component* const child = getChildComponent (0))
|
|
child->setBounds (getLocalBounds().reduced (2, 0));
|
|
}
|
|
|
|
void setHighlighted (bool shouldBeHighlighted)
|
|
{
|
|
shouldBeHighlighted = shouldBeHighlighted && itemInfo.isActive;
|
|
|
|
if (isHighlighted != shouldBeHighlighted)
|
|
{
|
|
isHighlighted = shouldBeHighlighted;
|
|
|
|
if (itemInfo.customComp != nullptr)
|
|
itemInfo.customComp->setHighlighted (shouldBeHighlighted);
|
|
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
PopupMenu::Item itemInfo;
|
|
|
|
private:
|
|
bool isHighlighted;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent)
|
|
};
|
|
|
|
//==============================================================================
|
|
class MenuWindow : public Component
|
|
{
|
|
public:
|
|
MenuWindow (const PopupMenu& menu, MenuWindow* const parentWindow,
|
|
const Options& opts,
|
|
const bool alignToRectangle,
|
|
const bool shouldDismissOnMouseUp,
|
|
ApplicationCommandManager** const manager)
|
|
: Component ("menu"),
|
|
parent (parentWindow),
|
|
options (opts),
|
|
managerOfChosenCommand (manager),
|
|
componentAttachedTo (options.targetComponent),
|
|
hasBeenOver (false),
|
|
needsToScroll (false),
|
|
dismissOnMouseUp (shouldDismissOnMouseUp),
|
|
hideOnExit (false),
|
|
disableMouseMoves (false),
|
|
hasAnyJuceCompHadFocus (false),
|
|
numColumns (0),
|
|
contentHeight (0),
|
|
childYOffset (0),
|
|
windowCreationTime (Time::getMillisecondCounter()),
|
|
lastFocusedTime (windowCreationTime),
|
|
timeEnteredCurrentChildComp (windowCreationTime)
|
|
{
|
|
setWantsKeyboardFocus (false);
|
|
setMouseClickGrabsKeyboardFocus (false);
|
|
setAlwaysOnTop (true);
|
|
|
|
setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel())
|
|
: menu.lookAndFeel);
|
|
|
|
setOpaque (getLookAndFeel().findColour (PopupMenu::backgroundColourId).isOpaque()
|
|
|| ! Desktop::canUseSemiTransparentWindows());
|
|
|
|
for (int i = 0; i < menu.items.size(); ++i)
|
|
{
|
|
PopupMenu::Item* const item = menu.items.getUnchecked(i);
|
|
|
|
if (i < menu.items.size() - 1 || ! item->isSeparator)
|
|
items.add (new ItemComponent (*item, options.standardHeight, *this));
|
|
}
|
|
|
|
calculateWindowPos (options.targetArea, alignToRectangle);
|
|
setTopLeftPosition (windowPos.getPosition());
|
|
updateYPositions();
|
|
|
|
if (options.visibleItemID != 0)
|
|
{
|
|
const int y = options.targetArea.getY() - windowPos.getY();
|
|
ensureItemIsVisible (options.visibleItemID,
|
|
isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1);
|
|
}
|
|
|
|
resizeToBestWindowPos();
|
|
addToDesktop (ComponentPeer::windowIsTemporary
|
|
| ComponentPeer::windowIgnoresKeyPresses
|
|
| getLookAndFeel().getMenuWindowFlags());
|
|
|
|
getActiveWindows().add (this);
|
|
Desktop::getInstance().addGlobalMouseListener (this);
|
|
}
|
|
|
|
~MenuWindow()
|
|
{
|
|
getActiveWindows().removeFirstMatchingValue (this);
|
|
Desktop::getInstance().removeGlobalMouseListener (this);
|
|
activeSubMenu = nullptr;
|
|
items.clear();
|
|
}
|
|
|
|
//==============================================================================
|
|
void paint (Graphics& g) override
|
|
{
|
|
if (isOpaque())
|
|
g.fillAll (Colours::white);
|
|
|
|
getLookAndFeel().drawPopupMenuBackground (g, getWidth(), getHeight());
|
|
}
|
|
|
|
void paintOverChildren (Graphics& g) override
|
|
{
|
|
if (canScroll())
|
|
{
|
|
LookAndFeel& lf = getLookAndFeel();
|
|
|
|
if (isTopScrollZoneActive())
|
|
lf.drawPopupMenuUpDownArrow (g, getWidth(), PopupMenuSettings::scrollZone, true);
|
|
|
|
if (isBottomScrollZoneActive())
|
|
{
|
|
g.setOrigin (0, getHeight() - PopupMenuSettings::scrollZone);
|
|
lf.drawPopupMenuUpDownArrow (g, getWidth(), PopupMenuSettings::scrollZone, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
// hide this and all sub-comps
|
|
void hide (const PopupMenu::Item* const item, const bool makeInvisible)
|
|
{
|
|
if (isVisible())
|
|
{
|
|
WeakReference<Component> deletionChecker (this);
|
|
|
|
activeSubMenu = nullptr;
|
|
currentChild = nullptr;
|
|
|
|
if (item != nullptr
|
|
&& item->commandManager != nullptr
|
|
&& item->itemID != 0)
|
|
{
|
|
*managerOfChosenCommand = item->commandManager;
|
|
}
|
|
|
|
exitModalState (item != nullptr ? item->itemID : 0);
|
|
|
|
if (makeInvisible && (deletionChecker != nullptr))
|
|
setVisible (false);
|
|
}
|
|
}
|
|
|
|
void dismissMenu (const PopupMenu::Item* const 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
|
|
const PopupMenu::Item mi (*item);
|
|
hide (&mi, false);
|
|
}
|
|
else
|
|
{
|
|
hide (nullptr, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
bool keyPressed (const KeyPress& key) override
|
|
{
|
|
if (key.isKeyCode (KeyPress::downKey))
|
|
{
|
|
selectNextItem (1);
|
|
}
|
|
else if (key.isKeyCode (KeyPress::upKey))
|
|
{
|
|
selectNextItem (-1);
|
|
}
|
|
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 (1);
|
|
}
|
|
else if (componentAttachedTo != nullptr)
|
|
{
|
|
componentAttachedTo->keyPressed (key);
|
|
}
|
|
}
|
|
else if (key.isKeyCode (KeyPress::returnKey))
|
|
{
|
|
triggerCurrentlyHighlightedItem();
|
|
}
|
|
else if (key.isKeyCode (KeyPress::escapeKey))
|
|
{
|
|
dismissMenu (nullptr);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void inputAttemptWhenModal() override
|
|
{
|
|
WeakReference<Component> deletionChecker (this);
|
|
|
|
for (int i = mouseSourceStates.size(); --i >= 0;)
|
|
{
|
|
mouseSourceStates.getUnchecked(i)->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 orginally 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.
|
|
const Point<int> mousePos (componentAttachedTo->getMouseXYRelative());
|
|
|
|
if (componentAttachedTo->reallyContains (mousePos, true))
|
|
{
|
|
postCommandMessage (PopupMenuSettings::dismissCommandId); // dismiss asynchrounously
|
|
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.targetComponent)
|
|
{
|
|
dismissMenu (nullptr);
|
|
return false;
|
|
}
|
|
|
|
if (MenuWindow* 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)
|
|
{
|
|
for (int i = mouseSourceStates.size(); --i >= 0;)
|
|
{
|
|
MouseSourceState& ms = *mouseSourceStates.getUnchecked(i);
|
|
if (ms.source == source)
|
|
return ms;
|
|
}
|
|
|
|
MouseSourceState* ms = new MouseSourceState (*this, source);
|
|
mouseSourceStates.add (ms);
|
|
return *ms;
|
|
}
|
|
|
|
//==============================================================================
|
|
bool isOverAnyMenu() const
|
|
{
|
|
return parent != nullptr ? parent->isOverAnyMenu()
|
|
: isOverChildren();
|
|
}
|
|
|
|
bool isOverChildren() const
|
|
{
|
|
return isVisible()
|
|
&& (isAnyMouseOver() || (activeSubMenu != nullptr && activeSubMenu->isOverChildren()));
|
|
}
|
|
|
|
bool isAnyMouseOver() const
|
|
{
|
|
for (int i = 0; i < mouseSourceStates.size(); ++i)
|
|
if (mouseSourceStates.getUnchecked(i)->isOver())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool treeContains (const MenuWindow* const window) const noexcept
|
|
{
|
|
const MenuWindow* mw = this;
|
|
|
|
while (mw->parent != nullptr)
|
|
mw = mw->parent;
|
|
|
|
while (mw != nullptr)
|
|
{
|
|
if (mw == window)
|
|
return true;
|
|
|
|
mw = mw->activeSubMenu;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool doesAnyJuceCompHaveFocus()
|
|
{
|
|
bool anyFocused = Process::isForegroundProcess();
|
|
|
|
if (anyFocused && Component::getCurrentlyFocusedComponent() == nullptr)
|
|
{
|
|
// because no component at all may have focus, our test here will
|
|
// only be triggered when something has focus and then loses it.
|
|
anyFocused = ! hasAnyJuceCompHadFocus;
|
|
|
|
for (int i = ComponentPeer::getNumPeers(); --i >= 0;)
|
|
{
|
|
if (ComponentPeer::getPeer (i)->isFocused())
|
|
{
|
|
anyFocused = true;
|
|
hasAnyJuceCompHadFocus = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return anyFocused;
|
|
}
|
|
|
|
//==============================================================================
|
|
void calculateWindowPos (const Rectangle<int>& target, const bool alignToRectangle)
|
|
{
|
|
const Rectangle<int> mon (Desktop::getInstance().getDisplays()
|
|
.getDisplayContaining (target.getCentre())
|
|
#if JUCE_MAC
|
|
.userArea);
|
|
#else
|
|
.totalArea); // on windows, don't stop the menu overlapping the taskbar
|
|
#endif
|
|
|
|
const int maxMenuHeight = mon.getHeight() - 24;
|
|
|
|
int x, y, widthToUse, heightToUse;
|
|
layoutMenuItems (mon.getWidth() - 24, maxMenuHeight, widthToUse, heightToUse);
|
|
|
|
if (alignToRectangle)
|
|
{
|
|
x = target.getX();
|
|
|
|
const int spaceUnder = mon.getHeight() - (target.getBottom() - mon.getY());
|
|
const int spaceOver = target.getY() - mon.getY();
|
|
|
|
if (heightToUse < spaceUnder - 30 || spaceUnder >= spaceOver)
|
|
y = target.getBottom();
|
|
else
|
|
y = target.getY() - heightToUse;
|
|
}
|
|
else
|
|
{
|
|
bool tendTowardsRight = target.getCentreX() < mon.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 < mon.getRight() - 4)
|
|
tendTowardsRight = true;
|
|
else if ((! parentGoingRight) && target.getX() > widthToUse + 4)
|
|
tendTowardsRight = false;
|
|
}
|
|
else if (target.getRight() + widthToUse < mon.getRight() - 32)
|
|
{
|
|
tendTowardsRight = true;
|
|
}
|
|
}
|
|
|
|
const int biggestSpace = jmax (mon.getRight() - target.getRight(),
|
|
target.getX() - mon.getX()) - 32;
|
|
|
|
if (biggestSpace < widthToUse)
|
|
{
|
|
layoutMenuItems (biggestSpace + target.getWidth() / 3, maxMenuHeight, widthToUse, heightToUse);
|
|
|
|
if (numColumns > 1)
|
|
layoutMenuItems (biggestSpace - 4, maxMenuHeight, widthToUse, heightToUse);
|
|
|
|
tendTowardsRight = (mon.getRight() - target.getRight()) >= (target.getX() - mon.getX());
|
|
}
|
|
|
|
if (tendTowardsRight)
|
|
x = jmin (mon.getRight() - widthToUse - 4, target.getRight());
|
|
else
|
|
x = jmax (mon.getX() + 4, target.getX() - widthToUse);
|
|
|
|
y = target.getY();
|
|
if (target.getCentreY() > mon.getCentreY())
|
|
y = jmax (mon.getY(), target.getBottom() - heightToUse);
|
|
}
|
|
|
|
x = jmax (mon.getX() + 1, jmin (mon.getRight() - (widthToUse + 6), x));
|
|
y = jmax (mon.getY() + 1, jmin (mon.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)
|
|
{
|
|
numColumns = 0;
|
|
contentHeight = 0;
|
|
int totalW;
|
|
|
|
const int maximumNumColumns = options.maxColumns > 0 ? options.maxColumns : 7;
|
|
|
|
do
|
|
{
|
|
++numColumns;
|
|
totalW = workOutBestSize (maxMenuW);
|
|
|
|
if (totalW > maxMenuW)
|
|
{
|
|
numColumns = jmax (1, numColumns - 1);
|
|
workOutBestSize (maxMenuW); // to update col widths
|
|
break;
|
|
}
|
|
else if (totalW > maxMenuW / 2 || contentHeight < maxMenuH)
|
|
{
|
|
break;
|
|
}
|
|
|
|
} while (numColumns < maximumNumColumns);
|
|
|
|
const int actualH = jmin (contentHeight, maxMenuH);
|
|
|
|
needsToScroll = contentHeight > actualH;
|
|
|
|
width = updateYPositions();
|
|
height = actualH + PopupMenuSettings::borderSize * 2;
|
|
}
|
|
|
|
int workOutBestSize (const int maxMenuW)
|
|
{
|
|
int totalW = 0;
|
|
contentHeight = 0;
|
|
int childNum = 0;
|
|
|
|
for (int col = 0; col < numColumns; ++col)
|
|
{
|
|
int colW = options.standardHeight, colH = 0;
|
|
|
|
const int 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 + PopupMenuSettings::borderSize * 2);
|
|
|
|
columnWidths.set (col, colW);
|
|
totalW += colW;
|
|
contentHeight = jmax (contentHeight, colH);
|
|
|
|
childNum += numChildren;
|
|
}
|
|
|
|
if (totalW < options.minWidth)
|
|
{
|
|
totalW = options.minWidth;
|
|
|
|
for (int col = 0; col < numColumns; ++col)
|
|
columnWidths.set (0, totalW / numColumns);
|
|
}
|
|
|
|
return totalW;
|
|
}
|
|
|
|
void ensureItemIsVisible (const int itemID, int wantedY)
|
|
{
|
|
jassert (itemID != 0);
|
|
|
|
for (int i = items.size(); --i >= 0;)
|
|
{
|
|
ItemComponent* const m = items.getUnchecked(i);
|
|
|
|
if (m != nullptr
|
|
&& m->itemInfo.itemID == itemID
|
|
&& windowPos.getHeight() > PopupMenuSettings::scrollZone * 4)
|
|
{
|
|
const int currentY = m->getY();
|
|
|
|
if (wantedY > 0 || currentY < 0 || m->getBottom() > windowPos.getHeight())
|
|
{
|
|
if (wantedY < 0)
|
|
wantedY = jlimit (PopupMenuSettings::scrollZone,
|
|
jmax (PopupMenuSettings::scrollZone,
|
|
windowPos.getHeight() - (PopupMenuSettings::scrollZone + m->getHeight())),
|
|
currentY);
|
|
|
|
const Rectangle<int> mon (Desktop::getInstance().getDisplays()
|
|
.getDisplayContaining (windowPos.getPosition()).userArea);
|
|
|
|
int deltaY = wantedY - currentY;
|
|
|
|
windowPos.setSize (jmin (windowPos.getWidth(), mon.getWidth()),
|
|
jmin (windowPos.getHeight(), mon.getHeight()));
|
|
|
|
const int newY = jlimit (mon.getY(),
|
|
mon.getBottom() - windowPos.getHeight(),
|
|
windowPos.getY() + deltaY);
|
|
|
|
deltaY -= newY - windowPos.getY();
|
|
|
|
childYOffset -= deltaY;
|
|
windowPos.setPosition (windowPos.getX(), newY);
|
|
|
|
updateYPositions();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void resizeToBestWindowPos()
|
|
{
|
|
Rectangle<int> r (windowPos);
|
|
|
|
if (childYOffset < 0)
|
|
{
|
|
r = r.withTop (r.getY() - childYOffset);
|
|
}
|
|
else if (childYOffset > 0)
|
|
{
|
|
const int spaceAtBottom = r.getHeight() - (contentHeight - childYOffset);
|
|
|
|
if (spaceAtBottom > 0)
|
|
r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom);
|
|
}
|
|
|
|
setBounds (r);
|
|
updateYPositions();
|
|
}
|
|
|
|
void alterChildYPos (const int delta)
|
|
{
|
|
if (canScroll())
|
|
{
|
|
childYOffset += delta;
|
|
|
|
if (delta < 0)
|
|
childYOffset = jmax (childYOffset, 0);
|
|
else if (delta > 0)
|
|
childYOffset = jmin (childYOffset,
|
|
contentHeight - windowPos.getHeight() + PopupMenuSettings::borderSize);
|
|
|
|
updateYPositions();
|
|
}
|
|
else
|
|
{
|
|
childYOffset = 0;
|
|
}
|
|
|
|
resizeToBestWindowPos();
|
|
repaint();
|
|
}
|
|
|
|
int updateYPositions()
|
|
{
|
|
int x = 0;
|
|
int childNum = 0;
|
|
|
|
for (int col = 0; col < numColumns; ++col)
|
|
{
|
|
const int numChildren = jmin (items.size() - childNum,
|
|
(items.size() + numColumns - 1) / numColumns);
|
|
|
|
const int colW = columnWidths [col];
|
|
|
|
int y = PopupMenuSettings::borderSize - (childYOffset + (getY() - windowPos.getY()));
|
|
|
|
for (int i = 0; i < numChildren; ++i)
|
|
{
|
|
Component* const c = items.getUnchecked (childNum + i);
|
|
c->setBounds (x, y, colW, c->getHeight());
|
|
y += c->getHeight();
|
|
}
|
|
|
|
x += colW;
|
|
childNum += numChildren;
|
|
}
|
|
|
|
return x;
|
|
}
|
|
|
|
void setCurrentlyHighlightedChild (ItemComponent* const child)
|
|
{
|
|
if (currentChild != nullptr)
|
|
currentChild->setHighlighted (false);
|
|
|
|
currentChild = child;
|
|
|
|
if (currentChild != nullptr)
|
|
{
|
|
currentChild->setHighlighted (true);
|
|
timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter();
|
|
}
|
|
}
|
|
|
|
bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); }
|
|
|
|
bool showSubMenuFor (ItemComponent* const childComp)
|
|
{
|
|
activeSubMenu = nullptr;
|
|
|
|
if (childComp != nullptr
|
|
&& childComp->itemInfo.hasActiveSubMenu())
|
|
{
|
|
activeSubMenu = new HelperClasses::MenuWindow (*(childComp->itemInfo.subMenu), this,
|
|
options.withTargetScreenArea (childComp->getScreenBounds())
|
|
.withMinimumWidth (0)
|
|
.withTargetComponent (nullptr),
|
|
false, dismissOnMouseUp, managerOfChosenCommand);
|
|
|
|
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
|
|
&& currentChild->itemInfo.canBeTriggered()
|
|
&& (currentChild->itemInfo.customComp == nullptr
|
|
|| currentChild->itemInfo.customComp->isTriggeredAutomatically()))
|
|
{
|
|
dismissMenu (¤tChild->itemInfo);
|
|
}
|
|
}
|
|
|
|
void selectNextItem (const int delta)
|
|
{
|
|
disableTimerUntilMouseMoves();
|
|
|
|
int start = jmax (0, items.indexOf (currentChild));
|
|
|
|
for (int i = items.size(); --i >= 0;)
|
|
{
|
|
start += delta;
|
|
|
|
if (ItemComponent* mic = items.getUnchecked ((start + items.size()) % items.size()))
|
|
{
|
|
if (mic->itemInfo.canBeTriggered() || mic->itemInfo.hasActiveSubMenu())
|
|
{
|
|
setCurrentlyHighlightedChild (mic);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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(); }
|
|
|
|
//==============================================================================
|
|
MenuWindow* parent;
|
|
const Options options;
|
|
OwnedArray<ItemComponent> items;
|
|
ApplicationCommandManager** managerOfChosenCommand;
|
|
WeakReference<Component> componentAttachedTo;
|
|
Rectangle<int> windowPos;
|
|
bool hasBeenOver, needsToScroll;
|
|
bool dismissOnMouseUp, hideOnExit, disableMouseMoves, hasAnyJuceCompHadFocus;
|
|
int numColumns, contentHeight, childYOffset;
|
|
Component::SafePointer<ItemComponent> currentChild;
|
|
ScopedPointer<MenuWindow> activeSubMenu;
|
|
Array<int> columnWidths;
|
|
uint32 windowCreationTime, lastFocusedTime, timeEnteredCurrentChildComp;
|
|
OwnedArray<MouseSourceState> mouseSourceStates;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow)
|
|
};
|
|
|
|
//==============================================================================
|
|
class MouseSourceState : private Timer
|
|
{
|
|
public:
|
|
MouseSourceState (MenuWindow& w, MouseInputSource s)
|
|
: window (w), source (s), scrollAcceleration (1.0),
|
|
lastScrollTime (Time::getMillisecondCounter()),
|
|
lastMouseMoveTime (0), isDown (false)
|
|
{
|
|
}
|
|
|
|
void handleMouseEvent (const MouseEvent& e)
|
|
{
|
|
if (! window.windowIsStillValid())
|
|
return;
|
|
|
|
startTimer (PopupMenuSettings::timerInterval);
|
|
handleMousePosition (e.getScreenPosition());
|
|
}
|
|
|
|
void timerCallback() override
|
|
{
|
|
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;
|
|
uint32 lastScrollTime, lastMouseMoveTime;
|
|
bool isDown;
|
|
|
|
void handleMousePosition (Point<int> globalMousePos)
|
|
{
|
|
const Point<int> localMousePos (window.getLocalPoint (nullptr, globalMousePos));
|
|
|
|
const uint32 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::getCurrentModifiers().isAnyMouseButtonDown()
|
|
|| ModifierKeys::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 bool 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)
|
|
{
|
|
Component* c = window.getComponentAt (localMousePos);
|
|
if (c == &window)
|
|
c = nullptr;
|
|
|
|
ItemComponent* 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)
|
|
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..
|
|
|
|
const Rectangle<int> itemScreenBounds (window.activeSubMenu->getScreenBounds());
|
|
float subX = (float) itemScreenBounds.getX();
|
|
|
|
Point<int> 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 += 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;
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MouseSourceState)
|
|
};
|
|
|
|
//==============================================================================
|
|
class NormalComponentWrapper : public PopupMenu::CustomComponent
|
|
{
|
|
public:
|
|
NormalComponentWrapper (Component* const comp, const int w, const int h,
|
|
const 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 (Component* const child = getChildComponent(0))
|
|
child->setBounds (getLocalBounds());
|
|
}
|
|
|
|
private:
|
|
const int width, height;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper)
|
|
};
|
|
|
|
//==============================================================================
|
|
class HeaderItemComponent : public PopupMenu::CustomComponent
|
|
{
|
|
public:
|
|
HeaderItemComponent (const String& name)
|
|
: PopupMenu::CustomComponent (false)
|
|
{
|
|
setName (name);
|
|
}
|
|
|
|
void paint (Graphics& g) override
|
|
{
|
|
getLookAndFeel().drawPopupMenuSectionHeader (g, getLocalBounds(), getName());
|
|
}
|
|
|
|
void getIdealSize (int& idealWidth, int& idealHeight)
|
|
{
|
|
getLookAndFeel().getIdealPopupMenuItemSize (getName(), false, -1, idealWidth, idealHeight);
|
|
idealHeight += idealHeight / 2;
|
|
idealWidth += idealWidth / 4;
|
|
}
|
|
|
|
private:
|
|
JUCE_LEAK_DETECTOR (HeaderItemComponent)
|
|
};
|
|
|
|
};
|
|
|
|
//==============================================================================
|
|
PopupMenu::PopupMenu()
|
|
: lookAndFeel (nullptr)
|
|
{
|
|
}
|
|
|
|
PopupMenu::PopupMenu (const PopupMenu& other)
|
|
: lookAndFeel (other.lookAndFeel)
|
|
{
|
|
items.addCopiesOf (other.items);
|
|
}
|
|
|
|
PopupMenu& PopupMenu::operator= (const PopupMenu& other)
|
|
{
|
|
if (this != &other)
|
|
{
|
|
lookAndFeel = other.lookAndFeel;
|
|
|
|
clear();
|
|
items.addCopiesOf (other.items);
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
#if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
|
|
PopupMenu::PopupMenu (PopupMenu&& other) noexcept
|
|
: lookAndFeel (other.lookAndFeel)
|
|
{
|
|
items.swapWith (other.items);
|
|
}
|
|
|
|
PopupMenu& PopupMenu::operator= (PopupMenu&& other) noexcept
|
|
{
|
|
jassert (this != &other); // hopefully the compiler should make this situation impossible!
|
|
|
|
items.swapWith (other.items);
|
|
lookAndFeel = other.lookAndFeel;
|
|
return *this;
|
|
}
|
|
#endif
|
|
|
|
PopupMenu::~PopupMenu()
|
|
{
|
|
}
|
|
|
|
void PopupMenu::clear()
|
|
{
|
|
items.clear();
|
|
}
|
|
|
|
void PopupMenu::addItem (int itemResultID, const String& itemText, bool isActive, bool isTicked)
|
|
{
|
|
jassert (itemResultID != 0); // 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..
|
|
|
|
items.add (new Item (itemResultID, itemText, isActive, isTicked, nullptr,
|
|
Colours::black, false, nullptr, nullptr, nullptr));
|
|
}
|
|
|
|
static Drawable* createDrawableFromImage (const Image& im)
|
|
{
|
|
if (im.isValid())
|
|
{
|
|
DrawableImage* d = new DrawableImage();
|
|
d->setImage (im);
|
|
return d;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void PopupMenu::addItem (int itemResultID, const String& itemText, bool isActive, bool isTicked, const Image& iconToUse)
|
|
{
|
|
jassert (itemResultID != 0); // 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..
|
|
|
|
|
|
items.add (new Item (itemResultID, itemText, isActive, isTicked, createDrawableFromImage (iconToUse),
|
|
Colours::black, false, nullptr, nullptr, nullptr));
|
|
}
|
|
|
|
void PopupMenu::addItem (int itemResultID, const String& itemText, bool isActive, bool isTicked, Drawable* iconToUse)
|
|
{
|
|
jassert (itemResultID != 0); // 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..
|
|
|
|
items.add (new Item (itemResultID, itemText, isActive, isTicked, iconToUse,
|
|
Colours::black, false, nullptr, nullptr, nullptr));
|
|
}
|
|
|
|
void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager,
|
|
const CommandID commandID,
|
|
const String& displayName)
|
|
{
|
|
jassert (commandManager != nullptr && commandID != 0);
|
|
|
|
if (const ApplicationCommandInfo* const registeredInfo = commandManager->getCommandForID (commandID))
|
|
{
|
|
ApplicationCommandInfo info (*registeredInfo);
|
|
ApplicationCommandTarget* const target = commandManager->getTargetForCommand (commandID, info);
|
|
|
|
items.add (new Item (commandID,
|
|
displayName.isNotEmpty() ? displayName
|
|
: info.shortName,
|
|
target != nullptr && (info.flags & ApplicationCommandInfo::isDisabled) == 0,
|
|
(info.flags & ApplicationCommandInfo::isTicked) != 0,
|
|
nullptr,
|
|
Colours::black,
|
|
false,
|
|
nullptr, nullptr,
|
|
commandManager));
|
|
}
|
|
}
|
|
|
|
void PopupMenu::addColouredItem (int itemResultID, const String& itemText, Colour itemTextColour,
|
|
bool isActive, bool isTicked, const Image& iconToUse)
|
|
{
|
|
jassert (itemResultID != 0); // 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..
|
|
|
|
items.add (new Item (itemResultID, itemText, isActive, isTicked, createDrawableFromImage (iconToUse),
|
|
itemTextColour, true, nullptr, nullptr, nullptr));
|
|
}
|
|
|
|
void PopupMenu::addCustomItem (int itemID, CustomComponent* cc, const PopupMenu* subMenu)
|
|
{
|
|
jassert (itemID != 0); // 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..
|
|
|
|
items.add (new Item (itemID, String::empty, true, false, nullptr,
|
|
Colours::black, false, cc, subMenu, nullptr));
|
|
}
|
|
|
|
void PopupMenu::addCustomItem (int itemResultID, Component* customComponent, int idealWidth, int idealHeight,
|
|
bool triggerMenuItemAutomaticallyWhenClicked, const PopupMenu* subMenu)
|
|
{
|
|
items.add (new Item (itemResultID, String::empty, true, false, nullptr, Colours::black, false,
|
|
new HelperClasses::NormalComponentWrapper (customComponent, idealWidth, idealHeight,
|
|
triggerMenuItemAutomaticallyWhenClicked),
|
|
subMenu, nullptr));
|
|
}
|
|
|
|
void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, bool isActive)
|
|
{
|
|
addSubMenu (subMenuName, subMenu, isActive, nullptr, false, 0);
|
|
}
|
|
|
|
void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, bool isActive,
|
|
const Image& iconToUse, bool isTicked, int itemResultID)
|
|
{
|
|
addSubMenu (subMenuName, subMenu, isActive, createDrawableFromImage (iconToUse), isTicked, itemResultID);
|
|
}
|
|
|
|
void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, bool isActive,
|
|
Drawable* iconToUse, bool isTicked, int itemResultID)
|
|
{
|
|
items.add (new Item (itemResultID, subMenuName, isActive && (itemResultID != 0 || subMenu.getNumItems() > 0), isTicked,
|
|
iconToUse, Colours::black, false, nullptr, &subMenu, nullptr));
|
|
}
|
|
|
|
void PopupMenu::addSeparator()
|
|
{
|
|
if (items.size() > 0 && ! items.getLast()->isSeparator)
|
|
items.add (new Item());
|
|
}
|
|
|
|
void PopupMenu::addSectionHeader (const String& title)
|
|
{
|
|
addCustomItem (PopupMenuSettings::sectionHeaderID, new HelperClasses::HeaderItemComponent (title));
|
|
}
|
|
|
|
//==============================================================================
|
|
PopupMenu::Options::Options()
|
|
: targetComponent (nullptr),
|
|
visibleItemID (0),
|
|
minWidth (0),
|
|
maxColumns (0),
|
|
standardHeight (0)
|
|
{
|
|
targetArea.setPosition (Desktop::getMousePosition());
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withTargetComponent (Component* comp) const noexcept
|
|
{
|
|
Options o (*this);
|
|
o.targetComponent = comp;
|
|
|
|
if (comp != nullptr)
|
|
o.targetArea = comp->getScreenBounds();
|
|
|
|
return o;
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withTargetScreenArea (const Rectangle<int>& area) const noexcept
|
|
{
|
|
Options o (*this);
|
|
o.targetArea = area;
|
|
return o;
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withMinimumWidth (int w) const noexcept
|
|
{
|
|
Options o (*this);
|
|
o.minWidth = w;
|
|
return o;
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withMaximumNumColumns (int cols) const noexcept
|
|
{
|
|
Options o (*this);
|
|
o.maxColumns = cols;
|
|
return o;
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withStandardItemHeight (int height) const noexcept
|
|
{
|
|
Options o (*this);
|
|
o.standardHeight = height;
|
|
return o;
|
|
}
|
|
|
|
PopupMenu::Options PopupMenu::Options::withItemThatMustBeVisible (int idOfItemToBeVisible) const noexcept
|
|
{
|
|
Options o (*this);
|
|
o.visibleItemID = idOfItemToBeVisible;
|
|
return o;
|
|
}
|
|
|
|
Component* PopupMenu::createWindow (const Options& options,
|
|
ApplicationCommandManager** managerOfChosenCommand) const
|
|
{
|
|
if (items.size() > 0)
|
|
return new HelperClasses::MenuWindow (*this, nullptr, options,
|
|
! options.targetArea.isEmpty(),
|
|
ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown(),
|
|
managerOfChosenCommand);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
//==============================================================================
|
|
// This invokes any command manager commands and deletes the menu window when it is dismissed
|
|
class PopupMenuCompletionCallback : public ModalComponentManager::Callback
|
|
{
|
|
public:
|
|
PopupMenuCompletionCallback()
|
|
: managerOfChosenCommand (nullptr),
|
|
prevFocused (Component::getCurrentlyFocusedComponent()),
|
|
prevTopLevel (prevFocused != nullptr ? prevFocused->getTopLevelComponent() : nullptr)
|
|
{
|
|
PopupMenuSettings::menuWasHiddenBecauseOfAppChange = false;
|
|
}
|
|
|
|
void modalStateFinished (int result)
|
|
{
|
|
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 = nullptr;
|
|
|
|
if (! PopupMenuSettings::menuWasHiddenBecauseOfAppChange)
|
|
{
|
|
if (prevTopLevel != nullptr)
|
|
prevTopLevel->toFront (true);
|
|
|
|
if (prevFocused != nullptr)
|
|
prevFocused->grabKeyboardFocus();
|
|
}
|
|
}
|
|
|
|
ApplicationCommandManager* managerOfChosenCommand;
|
|
ScopedPointer<Component> component;
|
|
WeakReference<Component> prevFocused, prevTopLevel;
|
|
|
|
private:
|
|
JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback)
|
|
};
|
|
|
|
int PopupMenu::showWithOptionalCallback (const Options& options, ModalComponentManager::Callback* const userCallback,
|
|
const bool canBeModal)
|
|
{
|
|
ScopedPointer<ModalComponentManager::Callback> userCallbackDeleter (userCallback);
|
|
ScopedPointer<PopupMenuCompletionCallback> callback (new PopupMenuCompletionCallback());
|
|
|
|
if (Component* window = createWindow (options, &(callback->managerOfChosenCommand)))
|
|
{
|
|
callback->component = window;
|
|
|
|
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, ModalComponentManager::Callback* userCallback)
|
|
{
|
|
#if ! JUCE_MODAL_LOOPS_PERMITTED
|
|
jassert (userCallback != nullptr);
|
|
#endif
|
|
|
|
showWithOptionalCallback (options, userCallback, false);
|
|
}
|
|
|
|
//==============================================================================
|
|
#if JUCE_MODAL_LOOPS_PERMITTED
|
|
int PopupMenu::show (const int itemIDThatMustBeVisible,
|
|
const int minimumWidth, const int maximumNumColumns,
|
|
const int standardItemHeight,
|
|
ModalComponentManager::Callback* callback)
|
|
{
|
|
return showWithOptionalCallback (Options().withItemThatMustBeVisible (itemIDThatMustBeVisible)
|
|
.withMinimumWidth (minimumWidth)
|
|
.withMaximumNumColumns (maximumNumColumns)
|
|
.withStandardItemHeight (standardItemHeight),
|
|
callback, true);
|
|
}
|
|
|
|
int PopupMenu::showAt (const Rectangle<int>& screenAreaToAttachTo,
|
|
const int itemIDThatMustBeVisible,
|
|
const int minimumWidth, const int maximumNumColumns,
|
|
const 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,
|
|
const int itemIDThatMustBeVisible,
|
|
const int minimumWidth, const int maximumNumColumns,
|
|
const int standardItemHeight,
|
|
ModalComponentManager::Callback* callback)
|
|
{
|
|
Options 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()
|
|
{
|
|
const Array<HelperClasses::MenuWindow*>& windows = HelperClasses::MenuWindow::getActiveWindows();
|
|
const int numWindows = windows.size();
|
|
|
|
for (int i = numWindows; --i >= 0;)
|
|
if (HelperClasses::MenuWindow* const pmw = windows[i])
|
|
pmw->dismissMenu (nullptr);
|
|
|
|
return numWindows > 0;
|
|
}
|
|
|
|
//==============================================================================
|
|
int PopupMenu::getNumItems() const noexcept
|
|
{
|
|
int num = 0;
|
|
|
|
for (int i = items.size(); --i >= 0;)
|
|
if (! items.getUnchecked(i)->isSeparator)
|
|
++num;
|
|
|
|
return num;
|
|
}
|
|
|
|
bool PopupMenu::containsCommandItem (const int commandID) const
|
|
{
|
|
for (int i = items.size(); --i >= 0;)
|
|
{
|
|
const Item& mi = *items.getUnchecked (i);
|
|
|
|
if ((mi.itemID == commandID && mi.commandManager != nullptr)
|
|
|| (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool PopupMenu::containsAnyActiveItems() const noexcept
|
|
{
|
|
for (int i = items.size(); --i >= 0;)
|
|
{
|
|
const Item& mi = *items.getUnchecked (i);
|
|
|
|
if (mi.subMenu != nullptr)
|
|
{
|
|
if (mi.subMenu->containsAnyActiveItems())
|
|
return true;
|
|
}
|
|
else if (mi.isActive)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel)
|
|
{
|
|
lookAndFeel = newLookAndFeel;
|
|
}
|
|
|
|
//==============================================================================
|
|
PopupMenu::CustomComponent::CustomComponent (bool autoTrigger)
|
|
: isHighlighted (false),
|
|
triggeredAutomatically (autoTrigger)
|
|
{
|
|
}
|
|
|
|
PopupMenu::CustomComponent::~CustomComponent()
|
|
{
|
|
}
|
|
|
|
void PopupMenu::CustomComponent::setHighlighted (bool shouldBeHighlighted)
|
|
{
|
|
isHighlighted = shouldBeHighlighted;
|
|
repaint();
|
|
}
|
|
|
|
void PopupMenu::CustomComponent::triggerMenuItem()
|
|
{
|
|
if (HelperClasses::ItemComponent* const mic = dynamic_cast<HelperClasses::ItemComponent*> (getParentComponent()))
|
|
{
|
|
if (HelperClasses::MenuWindow* const pmw = dynamic_cast<HelperClasses::MenuWindow*> (mic->getParentComponent()))
|
|
{
|
|
pmw->dismissMenu (&mic->itemInfo);
|
|
}
|
|
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::MenuItemIterator::MenuItemIterator (const PopupMenu& m)
|
|
: subMenu (nullptr),
|
|
itemId (0),
|
|
isSeparator (false),
|
|
isTicked (false),
|
|
isEnabled (false),
|
|
isCustomComponent (false),
|
|
isSectionHeader (false),
|
|
customColour (nullptr),
|
|
menu (m),
|
|
index (0)
|
|
{
|
|
}
|
|
|
|
PopupMenu::MenuItemIterator::~MenuItemIterator()
|
|
{
|
|
}
|
|
|
|
bool PopupMenu::MenuItemIterator::next()
|
|
{
|
|
if (index >= menu.items.size())
|
|
return false;
|
|
|
|
const Item* const item = menu.items.getUnchecked (index);
|
|
++index;
|
|
|
|
if (item->isSeparator && index >= menu.items.size()) // (avoid showing a separator at the end)
|
|
return false;
|
|
|
|
itemName = item->customComp != nullptr ? item->customComp->getName() : item->text;
|
|
subMenu = item->subMenu;
|
|
itemId = item->itemID;
|
|
isSeparator = item->isSeparator;
|
|
isTicked = item->isTicked;
|
|
isEnabled = item->isActive;
|
|
isSectionHeader = dynamic_cast<HelperClasses::HeaderItemComponent*> (static_cast<CustomComponent*> (item->customComp)) != nullptr;
|
|
isCustomComponent = (! isSectionHeader) && item->customComp != nullptr;
|
|
customColour = item->usesColour ? &(item->textColour) : nullptr;
|
|
icon = item->iconDrawable;
|
|
commandManager = item->commandManager;
|
|
|
|
return true;
|
|
}
|
|
|
|
void PopupMenu::MenuItemIterator::addItemTo (PopupMenu& targetMenu)
|
|
{
|
|
targetMenu.items.add (new Item (itemId, itemName, isEnabled, isTicked, icon != nullptr ? icon->createCopy() : nullptr,
|
|
customColour != nullptr ? *customColour : Colours::black,
|
|
customColour != nullptr, nullptr, subMenu, commandManager));
|
|
}
|
|
|