diff --git a/modules/juce_gui_basics/menus/juce_PopupMenu.cpp b/modules/juce_gui_basics/menus/juce_PopupMenu.cpp index 2418a5281b..ec1534a960 100644 --- a/modules/juce_gui_basics/menus/juce_PopupMenu.cpp +++ b/modules/juce_gui_basics/menus/juce_PopupMenu.cpp @@ -22,120 +22,68 @@ ============================================================================== */ -//============================================================================== + 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 +//============================================================================== +struct PopupMenu::HelperClasses { -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 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; - } +class MouseSourceState; +class MenuWindow; - shortcutKey = shortcutKey.trim(); +static bool canBeTriggered (const PopupMenu::Item& item) noexcept { return item.isEnabled && item.itemID != 0 && ! item.isSectionHeader; } +static bool hasActiveSubMenu (const PopupMenu::Item& item) noexcept { return item.isEnabled && item.subMenu != nullptr && item.subMenu->items.size() > 0; } +static const Colour* getColour (const PopupMenu::Item& item) noexcept { return item.colour != Colour (0x00000000) ? &item.colour : nullptr; } +static bool hasSubMenu (const PopupMenu::Item& item) noexcept { return item.subMenu != nullptr && (item.itemID == 0 || item.subMenu->getNumItems() > 0); } - if (shortcutKey.isNotEmpty()) - text << "" << shortcutKey; - } +//============================================================================== +struct HeaderItemComponent : public PopupMenu::CustomComponent +{ + HeaderItemComponent (const String& name) : PopupMenu::CustomComponent (false) + { + setName (name); } - 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 iconDrawable; - ReferenceCountedObjectPtr customComp; - ScopedPointer subMenu; - ApplicationCommandManager* const commandManager; + void paint (Graphics& g) override + { + getLookAndFeel().drawPopupMenuSectionHeader (g, getLocalBounds(), getName()); + } -private: - Item& operator= (const Item&); + void getIdealSize (int& idealWidth, int& idealHeight) override + { + getLookAndFeel().getIdealPopupMenuItemSize (getName(), false, -1, idealWidth, idealHeight); + idealHeight += idealHeight / 2; + idealWidth += idealWidth / 4; + } - JUCE_LEAK_DETECTOR (Item) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HeaderItemComponent) }; - //============================================================================== -struct PopupMenu::HelperClasses +struct ItemComponent : public Component { - -class MouseSourceState; -class MenuWindow; - -//============================================================================== -class ItemComponent : public Component -{ -public: - ItemComponent (const PopupMenu::Item& info, int standardItemHeight, MenuWindow& parent) - : itemInfo (info), + ItemComponent (const PopupMenu::Item& i, int standardItemHeight, MenuWindow& parent) + : item (i), + customComp (i.customComponent), isHighlighted (false) { - addAndMakeVisible (itemInfo.customComp); + if (item.isSectionHeader) + customComp = new HeaderItemComponent (item.text); + + addAndMakeVisible (customComp); + parent.addAndMakeVisible (this); + shortcutKeyDescription = getShortcutKeyDescription(); + int itemW = 80; int itemH = 16; getIdealSize (itemW, itemH, standardItemHeight); @@ -146,45 +94,33 @@ public: ~ItemComponent() { - removeChildComponent (itemInfo.customComp); + removeChildComponent (customComp); } void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight) { - if (itemInfo.customComp != nullptr) - itemInfo.customComp->getIdealSize (idealWidth, idealHeight); + if (customComp != nullptr) + customComp->getIdealSize (idealWidth, idealHeight); else - getLookAndFeel().getIdealPopupMenuItemSize (itemInfo.text, - itemInfo.isSeparator, + getLookAndFeel().getIdealPopupMenuItemSize (getTextForMeasurement(), + item.isSeparator, standardItemHeight, idealWidth, idealHeight); } void paint (Graphics& g) override { - if (itemInfo.customComp == nullptr) - { - String mainText (itemInfo.text); - String endText; - const int endIndex = mainText.indexOf (""); - - 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); - } + if (customComp == nullptr) + getLookAndFeel().drawPopupMenuItem (g, getLocalBounds(), + item.isSeparator, + item.isEnabled, + isHighlighted, + item.isTicked, + hasSubMenu (item), + item.text, + shortcutKeyDescription, + item.image, + getColour (item)); } void resized() override @@ -195,24 +131,60 @@ public: void setHighlighted (bool shouldBeHighlighted) { - shouldBeHighlighted = shouldBeHighlighted && itemInfo.isActive; + shouldBeHighlighted = shouldBeHighlighted && item.isEnabled; if (isHighlighted != shouldBeHighlighted) { isHighlighted = shouldBeHighlighted; - if (itemInfo.customComp != nullptr) - itemInfo.customComp->setHighlighted (shouldBeHighlighted); + if (customComp != nullptr) + customComp->setHighlighted (shouldBeHighlighted); repaint(); } } - PopupMenu::Item itemInfo; + PopupMenu::Item item; + String shortcutKeyDescription; private: + // NB: we use a copy of the one from the item info in case we're using our own section comp + ReferenceCountedObjectPtr customComp; bool isHighlighted; + String getShortcutKeyDescription() const + { + if (item.commandManager != nullptr && item.itemID != 0) + { + String shortcutKey; + const Array keyPresses (item.commandManager->getKeyMappings() + ->getKeyPressesAssignedToCommand (item.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; + } + + return shortcutKey.trim(); + } + + return String(); + } + + String getTextForMeasurement() const + { + return shortcutKeyDescription.isNotEmpty() ? item.text + " " + shortcutKeyDescription + : item.text; + } + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent) }; @@ -744,43 +716,43 @@ public: 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) + if (ItemComponent* const m = items.getUnchecked(i)) { - const int currentY = m->getY(); - - if (wantedY > 0 || currentY < 0 || m->getBottom() > windowPos.getHeight()) + if (m->item.itemID == itemID + && windowPos.getHeight() > PopupMenuSettings::scrollZone * 4) { - if (wantedY < 0) - wantedY = jlimit (PopupMenuSettings::scrollZone, - jmax (PopupMenuSettings::scrollZone, - windowPos.getHeight() - (PopupMenuSettings::scrollZone + m->getHeight())), - currentY); + const int currentY = m->getY(); - const Rectangle mon (Desktop::getInstance().getDisplays() - .getDisplayContaining (windowPos.getPosition()).userArea); + 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); - int deltaY = wantedY - currentY; + const Rectangle mon (Desktop::getInstance().getDisplays() + .getDisplayContaining (windowPos.getPosition()).userArea); - windowPos.setSize (jmin (windowPos.getWidth(), mon.getWidth()), - jmin (windowPos.getHeight(), mon.getHeight())); + int deltaY = wantedY - currentY; - const int newY = jlimit (mon.getY(), - mon.getBottom() - windowPos.getHeight(), - windowPos.getY() + deltaY); + windowPos.setSize (jmin (windowPos.getWidth(), mon.getWidth()), + jmin (windowPos.getHeight(), mon.getHeight())); - deltaY -= newY - windowPos.getY(); + const int newY = jlimit (mon.getY(), + mon.getBottom() - windowPos.getHeight(), + windowPos.getY() + deltaY); - childYOffset -= deltaY; - windowPos.setPosition (windowPos.getX(), newY); + deltaY -= newY - windowPos.getY(); - updateYPositions(); - } + childYOffset -= deltaY; + windowPos.setPosition (windowPos.getX(), newY); - break; + updateYPositions(); + } + + break; + } } } } @@ -877,9 +849,9 @@ public: activeSubMenu = nullptr; if (childComp != nullptr - && childComp->itemInfo.hasActiveSubMenu()) + && hasActiveSubMenu (childComp->item)) { - activeSubMenu = new HelperClasses::MenuWindow (*(childComp->itemInfo.subMenu), this, + activeSubMenu = new HelperClasses::MenuWindow (*(childComp->item.subMenu), this, options.withTargetScreenArea (childComp->getScreenBounds()) .withMinimumWidth (0) .withTargetComponent (nullptr), @@ -897,11 +869,11 @@ public: void triggerCurrentlyHighlightedItem() { if (currentChild != nullptr - && currentChild->itemInfo.canBeTriggered() - && (currentChild->itemInfo.customComp == nullptr - || currentChild->itemInfo.customComp->isTriggeredAutomatically())) + && canBeTriggered (currentChild->item) + && (currentChild->item.customComponent == nullptr + || currentChild->item.customComponent->isTriggeredAutomatically())) { - dismissMenu (¤tChild->itemInfo); + dismissMenu (¤tChild->item); } } @@ -917,7 +889,7 @@ public: if (ItemComponent* mic = items.getUnchecked ((start + items.size()) % items.size())) { - if (mic->itemInfo.canBeTriggered() || mic->itemInfo.hasActiveSubMenu()) + if (canBeTriggered (mic->item) || hasActiveSubMenu (mic->item)) { setCurrentlyHighlightedChild (mic); break; @@ -973,7 +945,7 @@ public: if (! window.windowIsStillValid()) return; - startTimer (PopupMenuSettings::timerInterval); + startTimerHz (20); handleMousePosition (e.getScreenPosition()); } @@ -1175,9 +1147,8 @@ private: }; //============================================================================== -class NormalComponentWrapper : public PopupMenu::CustomComponent +struct NormalComponentWrapper : public PopupMenu::CustomComponent { -public: NormalComponentWrapper (Component* const comp, const int w, const int h, const bool triggerMenuItemAutomaticallyWhenClicked) : PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked), @@ -1198,38 +1169,11 @@ public: 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) override - { - getLookAndFeel().getIdealPopupMenuItemSize (getName(), false, -1, idealWidth, idealHeight); - idealHeight += idealHeight / 2; - idealWidth += idealWidth / 4; - } - -private: - JUCE_LEAK_DETECTOR (HeaderItemComponent) -}; - }; //============================================================================== @@ -1282,14 +1226,68 @@ void PopupMenu::clear() items.clear(); } -void PopupMenu::addItem (int itemResultID, const String& itemText, bool isActive, bool isTicked) +//============================================================================== +PopupMenu::Item::Item() noexcept + : itemID (0), + commandManager (nullptr), + colour (0x00000000), + isEnabled (true), + isTicked (false), + isSeparator (false), + isSectionHeader (false) { - 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)); +PopupMenu::Item::Item (const Item& other) + : text (other.text), + itemID (other.itemID), + subMenu (createCopyIfNotNull (other.subMenu.get())), + image (other.image != nullptr ? other.image->createCopy() : nullptr), + customComponent (other.customComponent), + commandManager (other.commandManager), + colour (other.colour), + isEnabled (other.isEnabled), + isTicked (other.isTicked), + isSeparator (other.isSeparator), + isSectionHeader (other.isSectionHeader) +{ +} + +PopupMenu::Item& PopupMenu::Item::operator= (const Item& other) +{ + text = other.text; + itemID = other.itemID; + subMenu = createCopyIfNotNull (other.subMenu.get()); + image = (other.image != nullptr ? other.image->createCopy() : nullptr); + customComponent = other.customComponent; + commandManager = other.commandManager; + colour = other.colour; + isEnabled = other.isEnabled; + isTicked = other.isTicked; + isSeparator = other.isSeparator; + isSectionHeader = other.isSectionHeader; + return *this; +} + +void PopupMenu::addItem (const Item& newItem) +{ + // An ID of 0 is used as a return value to indicate that the user + // didn't pick anything, so you shouldn't use it as the ID for an item.. + jassert (newItem.itemID != 0 + || newItem.isSeparator || newItem.isSectionHeader + || newItem.subMenu != nullptr); + + items.add (new Item (newItem)); +} + +void PopupMenu::addItem (int itemResultID, const String& itemText, bool isActive, bool isTicked) +{ + Item i; + i.text = itemText; + i.itemID = itemResultID; + i.isEnabled = isActive; + i.isTicked = isTicked; + addItem (i); } static Drawable* createDrawableFromImage (const Image& im) @@ -1306,23 +1304,18 @@ static Drawable* createDrawableFromImage (const Image& im) 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)); + addItem (itemResultID, itemText, isActive, isTicked, createDrawableFromImage (iconToUse)); } 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)); + Item i; + i.text = itemText; + i.itemID = itemResultID; + i.isEnabled = isActive; + i.isTicked = isTicked; + i.image = iconToUse; + addItem (i); } void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager, @@ -1337,53 +1330,59 @@ void PopupMenu::addCommandItem (ApplicationCommandManager* commandManager, 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, - iconToUse, - Colours::black, - false, - nullptr, nullptr, - commandManager)); + Item i; + i.text = displayName.isNotEmpty() ? displayName : info.shortName; + i.itemID = (int) commandID; + i.commandManager = commandManager; + i.isEnabled = target != nullptr && (info.flags & ApplicationCommandInfo::isDisabled) == 0; + i.isTicked = (info.flags & ApplicationCommandInfo::isTicked) != 0; + i.image = iconToUse; + addItem (i); } } void PopupMenu::addColouredItem (int itemResultID, const String& itemText, Colour itemTextColour, 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, - itemTextColour, true, nullptr, nullptr, nullptr)); + Item i; + i.text = itemText; + i.itemID = itemResultID; + i.colour = itemTextColour; + i.isEnabled = isActive; + i.isTicked = isTicked; + i.image = iconToUse; + addItem (i); } void PopupMenu::addColouredItem (int itemResultID, const String& itemText, Colour itemTextColour, bool isActive, bool isTicked, const Image& iconToUse) { - addColouredItem (itemResultID, itemText, itemTextColour, isActive, isTicked, createDrawableFromImage (iconToUse)); + Item i; + i.text = itemText; + i.itemID = itemResultID; + i.colour = itemTextColour; + i.isEnabled = isActive; + i.isTicked = isTicked; + i.image = createDrawableFromImage (iconToUse); + addItem (i); } -void PopupMenu::addCustomItem (int itemID, CustomComponent* cc, const PopupMenu* subMenu) +void PopupMenu::addCustomItem (int itemResultID, 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)); + Item i; + i.itemID = itemResultID; + i.customComponent = cc; + i.subMenu = createCopyIfNotNull (subMenu); + addItem (i); } 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)); + addCustomItem (itemResultID, + new HelperClasses::NormalComponentWrapper (customComponent, idealWidth, idealHeight, + triggerMenuItemAutomaticallyWhenClicked), + subMenu); } void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, bool isActive) @@ -1400,19 +1399,32 @@ void PopupMenu::addSubMenu (const String& subMenuName, const PopupMenu& subMenu, 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)); + Item i; + i.text = subMenuName; + i.itemID = itemResultID; + i.subMenu = new PopupMenu (subMenu); + i.isEnabled = isActive && (itemResultID != 0 || subMenu.getNumItems() > 0); + i.isTicked = isTicked; + i.image = iconToUse; + addItem (i); } void PopupMenu::addSeparator() { if (items.size() > 0 && ! items.getLast()->isSeparator) - items.add (new Item()); + { + Item i; + i.isSeparator = true; + addItem (i); + } } void PopupMenu::addSectionHeader (const String& title) { - addCustomItem (PopupMenuSettings::sectionHeaderID, new HelperClasses::HeaderItemComponent (title)); + Item i; + i.text = title; + i.isSectionHeader = true; + addItem (i); } //============================================================================== @@ -1486,9 +1498,8 @@ Component* PopupMenu::createWindow (const Options& options, //============================================================================== // This invokes any command manager commands and deletes the menu window when it is dismissed -class PopupMenuCompletionCallback : public ModalComponentManager::Callback +struct PopupMenuCompletionCallback : public ModalComponentManager::Callback { -public: PopupMenuCompletionCallback() : managerOfChosenCommand (nullptr), prevFocused (Component::getCurrentlyFocusedComponent()), @@ -1524,7 +1535,6 @@ public: ScopedPointer component; WeakReference prevFocused, prevTopLevel; -private: JUCE_DECLARE_NON_COPYABLE (PopupMenuCompletionCallback) }; @@ -1651,10 +1661,8 @@ bool PopupMenu::containsCommandItem (const int commandID) const const Item& mi = *items.getUnchecked (i); if ((mi.itemID == commandID && mi.commandManager != nullptr) - || (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID))) - { + || (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID))) return true; - } } return false; @@ -1671,7 +1679,7 @@ bool PopupMenu::containsAnyActiveItems() const noexcept if (mi.subMenu->containsAnyActiveItems()) return true; } - else if (mi.isActive) + else if (mi.isEnabled) { return true; } @@ -1708,7 +1716,7 @@ void PopupMenu::CustomComponent::triggerMenuItem() { if (HelperClasses::MenuWindow* const pmw = mic->findParentComponentOfClass()) { - pmw->dismissMenu (&mic->itemInfo); + pmw->dismissMenu (&mic->item); } else { @@ -1725,53 +1733,21 @@ void PopupMenu::CustomComponent::triggerMenuItem() } //============================================================================== -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() -{ -} +PopupMenu::MenuItemIterator::MenuItemIterator (const PopupMenu& m) : 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; + const Item* const item = menu.items.getUnchecked (index++); - 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 (static_cast (item->customComp)) != nullptr; - isCustomComponent = (! isSectionHeader) && item->customComp != nullptr; - customColour = item->usesColour ? &(item->textColour) : nullptr; - icon = item->iconDrawable; - commandManager = item->commandManager; - - return true; + return ! (item->isSeparator && index >= menu.items.size()); // (avoid showing a separator at the end) } -void PopupMenu::MenuItemIterator::addItemTo (PopupMenu& targetMenu) +const PopupMenu::Item& PopupMenu::MenuItemIterator::getItem() const noexcept { - targetMenu.items.add (new Item (itemId, itemName, isEnabled, isTicked, icon != nullptr ? icon->createCopy() : nullptr, - customColour != nullptr ? *customColour : Colours::black, - customColour != nullptr, nullptr, subMenu, commandManager)); + jassert (isPositiveAndBelow (index - 1, menu.items.size())); + return *menu.items.getUnchecked (index - 1); } diff --git a/modules/juce_gui_basics/menus/juce_PopupMenu.h b/modules/juce_gui_basics/menus/juce_PopupMenu.h index f240723107..492aac2c73 100644 --- a/modules/juce_gui_basics/menus/juce_PopupMenu.h +++ b/modules/juce_gui_basics/menus/juce_PopupMenu.h @@ -104,6 +104,63 @@ public: /** Resets the menu, removing all its items. */ void clear(); + /** Describes a popup menu item. */ + struct Item + { + /** Creates a null item. + You'll need to set some fields after creating an Item before you + can add it to a PopupMenu + */ + Item() noexcept; + + /** Creates a copy of an item. */ + Item (const Item&); + + /** Creates a copy of an item. */ + Item& operator= (const Item&); + + /** The menu item's name. */ + String text; + + /** The menu item's ID. This can not be 0 if you want the item to be triggerable! */ + int itemID; + + /** A sub-menu, or nullptr if there isn't one. */ + ScopedPointer subMenu; + + /** A drawable to use as an icon, or nullptr if there isn't one. */ + ScopedPointer image; + + /** A custom component for the item to display, or nullptr if there isn't one. */ + ReferenceCountedObjectPtr customComponent; + + /** A command manager to use to automatically invoke the command, or nullptr if none is specified. */ + ApplicationCommandManager* commandManager; + + /** A colour to use to draw the menu text. + By default this is transparent black, which means that the LookAndFeel should choose the colour. + */ + Colour colour; + + /** True if this menu item is enabled. */ + bool isEnabled; + + /** True if this menu item should have a tick mark next to it. */ + bool isTicked; + + /** True if this menu item is a separator line. */ + bool isSeparator; + + /** True if this menu item is a section header. */ + bool isSectionHeader; + }; + + /** Adds an item to the menu. + You can call this method for full control over the item that is added, or use the other + addItem helper methods if you want to pass arguments rather than creating an Item object. + */ + void addItem (const Item& newItem); + /** Appends a new text item for this menu to show. @param itemResultID the number that will be returned from the show() method @@ -274,7 +331,6 @@ public: int itemResultID = 0); /** Appends a separator to the menu, to help break it up into sections. - The menu class is smart enough not to display separators at the top or bottom of the menu, and it will replace mutliple adjacent separators with a single one, so your code can be quite free and easy about adding these, and it'll @@ -283,14 +339,12 @@ public: void addSeparator(); /** Adds a non-clickable text item to the menu. - This is a bold-font items which can be used as a header to separate the items into named groups. */ void addSectionHeader (const String& title); /** Returns the number of items that the menu currently contains. - (This doesn't count separators). */ int getNumItems() const noexcept; @@ -486,21 +540,10 @@ public: */ bool next(); - /** Adds an item to the target menu which has all the properties of this item. */ - void addItemTo (PopupMenu& targetMenu); - - //============================================================================== - String itemName; - const PopupMenu* subMenu; - int itemId; - bool isSeparator; - bool isTicked; - bool isEnabled; - bool isCustomComponent; - bool isSectionHeader; - const Colour* customColour; - const Drawable* icon; - ApplicationCommandManager* commandManager; + /** Returns a reference to the description of the current item. + It is only valid to call this after next() has returned true! + */ + const Item& getItem() const noexcept; private: //============================================================================== @@ -621,7 +664,6 @@ public: private: //============================================================================== - JUCE_PUBLIC_IN_DLL_BUILD (class Item) JUCE_PUBLIC_IN_DLL_BUILD (struct HelperClasses) friend struct HelperClasses; friend class MenuBarComponent; diff --git a/modules/juce_gui_basics/native/juce_mac_MainMenu.mm b/modules/juce_gui_basics/native/juce_mac_MainMenu.mm index 8ca3b614f7..9888a8e671 100644 --- a/modules/juce_gui_basics/native/juce_mac_MainMenu.mm +++ b/modules/juce_gui_basics/native/juce_mac_MainMenu.mm @@ -181,16 +181,17 @@ public: void addMenuItem (PopupMenu::MenuItemIterator& iter, NSMenu* menuToAddTo, const int topLevelMenuId, const int topLevelIndex) { - NSString* text = juceStringToNS (iter.itemName.upToFirstOccurrenceOf ("", false, true)); + const PopupMenu::Item& i = iter.getItem(); + NSString* text = juceStringToNS (i.text); if (text == nil) text = nsEmptyString(); - if (iter.isSeparator) + if (i.isSeparator) { [menuToAddTo addItem: [NSMenuItem separatorItem]]; } - else if (iter.isSectionHeader) + else if (i.isSectionHeader) { NSMenuItem* item = [menuToAddTo addItemWithTitle: text action: nil @@ -198,9 +199,9 @@ public: [item setEnabled: false]; } - else if (iter.subMenu != nullptr) + else if (i.subMenu != nullptr) { - if (iter.itemName == recentItemsMenuName) + if (i.text == recentItemsMenuName) { if (recent == nullptr) recent = new RecentFilesMenuItem(); @@ -219,10 +220,10 @@ public: action: nil keyEquivalent: nsEmptyString()]; - [item setTag: iter.itemId]; - [item setEnabled: iter.isEnabled]; + [item setTag: i.itemID]; + [item setEnabled: i.isEnabled]; - NSMenu* sub = createMenu (*iter.subMenu, iter.itemName, topLevelMenuId, topLevelIndex, false); + NSMenu* sub = createMenu (*i.subMenu, i.text, topLevelMenuId, topLevelIndex, false); [menuToAddTo setSubmenu: sub forItem: item]; [sub release]; } @@ -232,19 +233,19 @@ public: action: @selector (menuItemInvoked:) keyEquivalent: nsEmptyString()]; - [item setTag: iter.itemId]; - [item setEnabled: iter.isEnabled]; - [item setState: iter.isTicked ? NSOnState : NSOffState]; + [item setTag: i.itemID]; + [item setEnabled: i.isEnabled]; + [item setState: i.isTicked ? NSOnState : NSOffState]; [item setTarget: (id) callback]; - NSMutableArray* info = [NSMutableArray arrayWithObject: [NSNumber numberWithUnsignedLongLong: (pointer_sized_uint) (void*) iter.commandManager]]; + NSMutableArray* info = [NSMutableArray arrayWithObject: [NSNumber numberWithUnsignedLongLong: (pointer_sized_uint) (void*) i.commandManager]]; [info addObject: [NSNumber numberWithInt: topLevelIndex]]; [item setRepresentedObject: info]; - if (iter.commandManager != nullptr) + if (i.commandManager != nullptr) { - const Array keyPresses (iter.commandManager->getKeyMappings() - ->getKeyPressesAssignedToCommand (iter.itemId)); + const Array keyPresses (i.commandManager->getKeyMappings() + ->getKeyPressesAssignedToCommand (i.itemID)); if (keyPresses.size() > 0) {