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.
 
 
 
 
 
 
dexed/JuceLibraryCode/modules/juce_gui_basics/menus/juce_PopupMenu.cpp

1775 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 (&currentChild->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());
}
bool isOver() const
{
return window.reallyContains (window.getLocalPoint (nullptr, source.getScreenPosition()), 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
{
g.setFont (getLookAndFeel().getPopupMenuFont().boldened());
g.setColour (findColour (PopupMenu::headerTextColourId));
g.drawFittedText (getName(),
12, 0, getWidth() - 16, proportionOfHeight (0.8f),
Justification::bottomLeft, 1);
}
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));
}