From 2acd7513b31ef47a219bc568a647e2eea65645ca Mon Sep 17 00:00:00 2001 From: GavinFAW Date: Wed, 6 Dec 2023 12:55:09 +0100 Subject: [PATCH 1/7] updating popmenu --- .../juce_gui_basics/menus/juce_PopupMenu.cpp | 10 ++++++ .../juce_gui_basics/menus/juce_PopupMenu.h | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/modules/juce_gui_basics/menus/juce_PopupMenu.cpp b/modules/juce_gui_basics/menus/juce_PopupMenu.cpp index 8dd135733a..55f435c0e9 100644 --- a/modules/juce_gui_basics/menus/juce_PopupMenu.cpp +++ b/modules/juce_gui_basics/menus/juce_PopupMenu.cpp @@ -2357,6 +2357,16 @@ bool PopupMenu::MenuItemIterator::next() return true; } +PopupMenu::Options PopupMenu::Options::withSelectableAreaLeftInset (int xInsetAmount) const +{ + return with (*this, &Options::selectableAreaLeftInset, xInsetAmount); +} + +PopupMenu::Options PopupMenu::Options::withSelectableAreaRightInset (int xInsetAmount) const +{ + return with (*this, &Options::selectableAreaRightInset, xInsetAmount); +} + PopupMenu::Item& PopupMenu::MenuItemIterator::getItem() const { jassert (currentItem != nullptr); diff --git a/modules/juce_gui_basics/menus/juce_PopupMenu.h b/modules/juce_gui_basics/menus/juce_PopupMenu.h index 6ca8c721d7..04c2f58f1f 100644 --- a/modules/juce_gui_basics/menus/juce_PopupMenu.h +++ b/modules/juce_gui_basics/menus/juce_PopupMenu.h @@ -551,6 +551,37 @@ public: /** Sets the direction of the popup menu relative to the target screen area. */ [[nodiscard]] Options withPreferredPopupDirection (PopupDirection direction) const; + + + /** Provide an X value from the left edge of any PopupMenu item such + that clicks to the left of the X value will NOT select the Popup + menu item, but clicks to the right will select the Popup men item. + + This is useful for blocking off area for extra UI in a + PopupMenu::CustomComponent that you do not want to be used for + selecting a menu item. + + @note Added by Tim for FAW SampleComboBox.h so that we could prevent + the sample audio preview buttons in the SamplePopMenuItem + from selecting the item. + */ + [[nodiscard]] Options withSelectableAreaLeftInset (int xInsetAmount) const; + + + /** Provide an X value from the right edge of any PopupMenu item such + that clicks to the right of the X value will NOT select the Popup + menu item, but clicks to the left will select the Popup men item. + + This is useful for blocking off area for extra UI in a + PopupMenu::CustomComponent that you do not want to be used for + selecting a menu item. + + @note Added by Tim for FAW SampleComboBox.h so that we could prevent + the favorite buttons in the SamplePopMenuItem from selecting + the item. + */ + [[nodiscard]] Options withSelectableAreaRightInset (int xInsetAmount) const; + /** Sets an item to select in the menu. From 0f926d89388710357e3ea5b074af39daec996b11 Mon Sep 17 00:00:00 2001 From: GavinFAW Date: Wed, 6 Dec 2023 14:24:42 +0100 Subject: [PATCH 2/7] adding in changes so PopMenu builds --- .../juce_gui_basics/menus/juce_PopupMenu.cpp | 2950 +++++++++-------- .../juce_gui_basics/menus/juce_PopupMenu.h | 1980 +++++------ 2 files changed, 2479 insertions(+), 2451 deletions(-) diff --git a/modules/juce_gui_basics/menus/juce_PopupMenu.cpp b/modules/juce_gui_basics/menus/juce_PopupMenu.cpp index 55f435c0e9..b2403c4e23 100644 --- a/modules/juce_gui_basics/menus/juce_PopupMenu.cpp +++ b/modules/juce_gui_basics/menus/juce_PopupMenu.cpp @@ -38,1562 +38,1576 @@ namespace PopupMenuSettings struct PopupMenu::HelperClasses { -class MouseSourceState; -struct MenuWindow; + class MouseSourceState; + struct MenuWindow; -static bool canBeTriggered (const PopupMenu::Item& item) noexcept -{ - return item.isEnabled - && item.itemID != 0 - && ! item.isSectionHeader - && (item.customComponent == nullptr || item.customComponent->isTriggeredAutomatically()); -} - -static bool hasActiveSubMenu (const PopupMenu::Item& item) noexcept -{ - return item.isEnabled - && item.subMenu != nullptr - && item.subMenu->items.size() > 0; -} - -//============================================================================== -struct HeaderItemComponent : public PopupMenu::CustomComponent -{ - HeaderItemComponent (const String& name, const Options& opts) - : CustomComponent (false), options (opts) + static bool canBeTriggered (const PopupMenu::Item& item) noexcept { - setName (name); + return item.isEnabled + && item.itemID != 0 + && ! item.isSectionHeader + && (item.customComponent == nullptr || item.customComponent->isTriggeredAutomatically()); } - void paint (Graphics& g) override + static bool hasActiveSubMenu (const PopupMenu::Item& item) noexcept { - getLookAndFeel().drawPopupMenuSectionHeaderWithOptions (g, - getLocalBounds(), - getName(), - options); + return item.isEnabled + && item.subMenu != nullptr + && item.subMenu->items.size() > 0; } - void getIdealSize (int& idealWidth, int& idealHeight) override - { - getLookAndFeel().getIdealPopupMenuItemSizeWithOptions (getName(), - false, - -1, - idealWidth, - idealHeight, - options); - idealHeight += idealHeight / 2; - idealWidth += idealWidth / 4; - } - - std::unique_ptr createAccessibilityHandler() override - { - return createIgnoredAccessibilityHandler (*this); - } - - const Options& options; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HeaderItemComponent) -}; - -//============================================================================== -struct ItemComponent : public Component -{ - ItemComponent (const PopupMenu::Item& i, const PopupMenu::Options& o, MenuWindow& parent) - : item (i), parentWindow (parent), options (o), customComp (i.customComponent) - { - if (item.isSectionHeader) - customComp = *new HeaderItemComponent (item.text, options); - - if (customComp != nullptr) - { - setItem (*customComp, &item); - addAndMakeVisible (*customComp); - } - - parent.addAndMakeVisible (this); - - updateShortcutKeyDescription(); - - int itemW = 80; - int itemH = 16; - getIdealSize (itemW, itemH, options.getStandardItemHeight()); - setSize (itemW, jlimit (1, 600, itemH)); - - addMouseListener (&parent, false); - } - - ~ItemComponent() override - { - if (customComp != nullptr) - setItem (*customComp, nullptr); - - removeChildComponent (customComp.get()); - } - - void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight) - { - if (customComp != nullptr) - customComp->getIdealSize (idealWidth, idealHeight); - else - getLookAndFeel().getIdealPopupMenuItemSizeWithOptions (getTextForMeasurement(), - item.isSeparator, - standardItemHeight, - idealWidth, idealHeight, - options); - } - - void paint (Graphics& g) override - { - if (customComp == nullptr) - getLookAndFeel().drawPopupMenuItemWithOptions (g, getLocalBounds(), - isHighlighted, - item, - options); - } - - void resized() override - { - if (auto* child = getChildComponent (0)) - { - const auto border = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options); - child->setBounds (getLocalBounds().reduced (border, 0)); - } - } - - void setHighlighted (bool shouldBeHighlighted) - { - shouldBeHighlighted = shouldBeHighlighted && item.isEnabled; - - if (isHighlighted != shouldBeHighlighted) - { - isHighlighted = shouldBeHighlighted; - - if (customComp != nullptr) - customComp->setHighlighted (shouldBeHighlighted); - - if (isHighlighted) - if (auto* handler = getAccessibilityHandler()) - handler->grabFocus(); - - repaint(); - } - } - - static bool isAccessibilityHandlerRequired (const PopupMenu::Item& item) - { - return item.isSectionHeader || hasActiveSubMenu (item) || canBeTriggered (item); - } - - PopupMenu::Item item; - -private: //============================================================================== - class ItemAccessibilityHandler : public AccessibilityHandler + struct HeaderItemComponent : public PopupMenu::CustomComponent { - public: - explicit ItemAccessibilityHandler (ItemComponent& itemComponentToWrap) - : AccessibilityHandler (itemComponentToWrap, - isAccessibilityHandlerRequired (itemComponentToWrap.item) ? AccessibilityRole::menuItem - : AccessibilityRole::ignored, - getAccessibilityActions (*this, itemComponentToWrap)), - itemComponent (itemComponentToWrap) + HeaderItemComponent (const String& name, const Options& opts) + : CustomComponent (false), options (opts) { + setName (name); } - String getTitle() const override + void paint (Graphics& g) override { - return itemComponent.item.text; + getLookAndFeel().drawPopupMenuSectionHeaderWithOptions (g, + getLocalBounds(), + getName(), + options); } - AccessibleState getCurrentState() const override + void getIdealSize (int& idealWidth, int& idealHeight) override { - auto state = AccessibilityHandler::getCurrentState().withSelectable() - .withAccessibleOffscreen(); - - if (hasActiveSubMenu (itemComponent.item)) - { - state = itemComponent.parentWindow.isSubMenuVisible() ? state.withExpandable().withExpanded() - : state.withExpandable().withCollapsed(); - } - - if (itemComponent.item.isTicked) - state = state.withCheckable().withChecked(); - - return state.isFocused() ? state.withSelected() : state; + getLookAndFeel().getIdealPopupMenuItemSizeWithOptions (getName(), + false, + -1, + idealWidth, + idealHeight, + options); + idealHeight += idealHeight / 2; + idealWidth += idealWidth / 4; } - private: - static AccessibilityActions getAccessibilityActions (ItemAccessibilityHandler& handler, - ItemComponent& item) + std::unique_ptr createAccessibilityHandler() override { - auto onFocus = [&item] - { - item.parentWindow.disableTimerUntilMouseMoves(); - item.parentWindow.ensureItemComponentIsVisible (item, -1); - item.parentWindow.setCurrentlyHighlightedChild (&item); - }; - - auto onToggle = [&handler, &item, onFocus] - { - if (handler.getCurrentState().isSelected()) - item.parentWindow.setCurrentlyHighlightedChild (nullptr); - else - onFocus(); - }; - - auto actions = AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus)) - .addAction (AccessibilityActionType::toggle, std::move (onToggle)); - - if (canBeTriggered (item.item)) - { - actions.addAction (AccessibilityActionType::press, [&item] - { - item.parentWindow.setCurrentlyHighlightedChild (&item); - item.parentWindow.triggerCurrentlyHighlightedItem(); - }); - } - - if (hasActiveSubMenu (item.item)) - { - auto showSubMenu = [&item] - { - item.parentWindow.showSubMenuFor (&item); - - if (auto* subMenu = item.parentWindow.activeSubMenu.get()) - subMenu->setCurrentlyHighlightedChild (subMenu->items.getFirst()); - }; - - actions.addAction (AccessibilityActionType::press, showSubMenu); - actions.addAction (AccessibilityActionType::showMenu, showSubMenu); - } - - return actions; + return nullptr; } - ItemComponent& itemComponent; + const Options& options; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HeaderItemComponent) }; - std::unique_ptr createAccessibilityHandler() override + //============================================================================== + struct ItemComponent : public Component { - return item.isSeparator ? createIgnoredAccessibilityHandler (*this) - : std::make_unique (*this); - } + ItemComponent (const PopupMenu::Item& i, const PopupMenu::Options& o, MenuWindow& parent) + : item (i), parentWindow (parent), options (o), customComp (i.customComponent) + { + if (item.isSectionHeader) + customComp = *new HeaderItemComponent (item.text, options); + + if (customComp != nullptr) + { + setItem (*customComp, &item); + addAndMakeVisible (*customComp); + } + + parent.addAndMakeVisible (this); + + updateShortcutKeyDescription(); + + int itemW = 80; + int itemH = 16; + getIdealSize (itemW, itemH, options.getStandardItemHeight()); + setSize (itemW, jlimit (1, 600, itemH)); + + addMouseListener (&parent, false); + } + + ~ItemComponent() override + { + if (customComp != nullptr) + setItem (*customComp, nullptr); + + removeChildComponent (customComp.get()); + } + + void getIdealSize (int& idealWidth, int& idealHeight, const int standardItemHeight) + { + if (customComp != nullptr) + customComp->getIdealSize (idealWidth, idealHeight); + else + getLookAndFeel().getIdealPopupMenuItemSizeWithOptions (getTextForMeasurement(), + item.isSeparator, + standardItemHeight, + idealWidth, idealHeight, + options); + } + + void paint (Graphics& g) override + { + if (customComp == nullptr) + getLookAndFeel().drawPopupMenuItemWithOptions (g, getLocalBounds(), + isHighlighted, + item, + options); + } + + void resized() override + { + if (auto* child = getChildComponent (0)) + { + const auto border = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options); + child->setBounds (getLocalBounds().reduced (border, 0)); + } + } + + void setHighlighted (bool shouldBeHighlighted) + { + shouldBeHighlighted = shouldBeHighlighted && item.isEnabled; + + if (isHighlighted != shouldBeHighlighted) + { + isHighlighted = shouldBeHighlighted; + + if (customComp != nullptr) + customComp->setHighlighted (shouldBeHighlighted); + + if (isHighlighted) + if (auto* handler = getAccessibilityHandler()) + handler->grabFocus(); + + repaint(); + } + } + + static bool isAccessibilityHandlerRequired (const PopupMenu::Item& item) + { + return item.isSectionHeader || hasActiveSubMenu (item) || canBeTriggered (item); + } + + PopupMenu::Item item; + + private: + class ValueInterface : public AccessibilityValueInterface + { + public: + ValueInterface() = default; + + bool isReadOnly() const override { return true; } + + double getCurrentValue() const override + { + return 1.0; + } + + String getCurrentValueAsString() const override + { + return TRANS ("Checked"); + } + + void setValue (double) override {} + void setValueAsString (const String&) override {} + + AccessibleValueRange getRange() const override { return {}; } + }; + + //============================================================================== + class ItemAccessibilityHandler : public AccessibilityHandler + { + public: + explicit ItemAccessibilityHandler (ItemComponent& itemComponentToWrap) + : AccessibilityHandler (itemComponentToWrap, + isAccessibilityHandlerRequired (itemComponentToWrap.item) ? AccessibilityRole::menuItem + : AccessibilityRole::ignored, + getAccessibilityActions (*this, itemComponentToWrap), + AccessibilityHandler::Interfaces { itemComponentToWrap.item.isTicked ? std::make_unique() + : nullptr }), + itemComponent (itemComponentToWrap) + { + } + + String getTitle() const override + { + return itemComponent.item.text; + } + + AccessibleState getCurrentState() const override + { + auto state = AccessibilityHandler::getCurrentState().withSelectable() + .withAccessibleOffscreen(); + + if (hasActiveSubMenu (itemComponent.item)) + { + state = itemComponent.parentWindow.isSubMenuVisible() ? state.withExpandable().withExpanded() + : state.withExpandable().withCollapsed(); + } + + if (itemComponent.item.isTicked) + state = state.withChecked(); + + return state.isFocused() ? state.withSelected() : state; + } + + private: + static AccessibilityActions getAccessibilityActions (ItemAccessibilityHandler& handler, + ItemComponent& item) + { + auto onFocus = [&item] + { + item.parentWindow.disableTimerUntilMouseMoves(); + item.parentWindow.ensureItemComponentIsVisible (item, -1); + item.parentWindow.setCurrentlyHighlightedChild (&item); + }; + + auto onToggle = [&handler, &item, onFocus] + { + if (handler.getCurrentState().isSelected()) + item.parentWindow.setCurrentlyHighlightedChild (nullptr); + else + onFocus(); + }; + + auto actions = AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus)) + .addAction (AccessibilityActionType::toggle, std::move (onToggle)); + + if (canBeTriggered (item.item)) + { + actions.addAction (AccessibilityActionType::press, [&item] + { + item.parentWindow.setCurrentlyHighlightedChild (&item); + item.parentWindow.triggerCurrentlyHighlightedItem(); + }); + } + + if (hasActiveSubMenu (item.item)) + { + auto showSubMenu = [&item] + { + item.parentWindow.showSubMenuFor (&item); + + if (auto* subMenu = item.parentWindow.activeSubMenu.get()) + subMenu->setCurrentlyHighlightedChild (subMenu->items.getFirst()); + }; + + actions.addAction (AccessibilityActionType::press, showSubMenu); + actions.addAction (AccessibilityActionType::showMenu, showSubMenu); + } + + return actions; + } + + ItemComponent& itemComponent; + }; + + std::unique_ptr createAccessibilityHandler() override + { + return item.isSeparator ? nullptr : std::make_unique (*this); + } + + //============================================================================== + MenuWindow& parentWindow; + const PopupMenu::Options& options; + // NB: we use a copy of the one from the item info in case we're using our own section comp + ReferenceCountedObjectPtr customComp; + bool isHighlighted = false; + + void updateShortcutKeyDescription() + { + if (item.commandManager != nullptr + && item.itemID != 0 + && item.shortcutKeyDescription.isEmpty()) + { + String shortcutKey; + + for (auto& keypress : item.commandManager->getKeyMappings() + ->getKeyPressesAssignedToCommand (item.itemID)) + { + auto key = keypress.getTextDescriptionWithIcons(); + + if (shortcutKey.isNotEmpty()) + shortcutKey << ", "; + + if (key.length() == 1 && key[0] < 128) + shortcutKey << "shortcut: '" << key << '\''; + else + shortcutKey << key; + } + + item.shortcutKeyDescription = shortcutKey.trim(); + } + } + + String getTextForMeasurement() const + { + return item.shortcutKeyDescription.isNotEmpty() ? item.text + " " + item.shortcutKeyDescription + : item.text; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent) + }; //============================================================================== - MenuWindow& parentWindow; - const PopupMenu::Options& options; - // NB: we use a copy of the one from the item info in case we're using our own section comp - ReferenceCountedObjectPtr customComp; - bool isHighlighted = false; - - void updateShortcutKeyDescription() + struct MenuWindow : public Component { - if (item.commandManager != nullptr - && item.itemID != 0 - && item.shortcutKeyDescription.isEmpty()) + MenuWindow (const PopupMenu& menu, MenuWindow* parentWindow, + Options opts, bool alignToRectangle, bool shouldDismissOnMouseUp, + ApplicationCommandManager** manager, float parentScaleFactor = 1.0f) + : Component ("menu"), + parent (parentWindow), + options (opts.withParentComponent (getLookAndFeel().getParentComponentForMenuOptions (opts))), + managerOfChosenCommand (manager), + componentAttachedTo (options.getTargetComponent()), + dismissOnMouseUp (shouldDismissOnMouseUp), + windowCreationTime (Time::getMillisecondCounter()), + lastFocusedTime (windowCreationTime), + timeEnteredCurrentChildComp (windowCreationTime), + scaleFactor (parentWindow != nullptr ? parentScaleFactor : 1.0f) { - String shortcutKey; + setWantsKeyboardFocus (false); + setMouseClickGrabsKeyboardFocus (false); + setAlwaysOnTop (true); + setFocusContainerType (FocusContainerType::focusContainer); - for (auto& keypress : item.commandManager->getKeyMappings() - ->getKeyPressesAssignedToCommand (item.itemID)) + setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel()) + : menu.lookAndFeel.get()); + + auto& lf = getLookAndFeel(); + + if (auto* pc = options.getParentComponent()) { - auto key = keypress.getTextDescriptionWithIcons(); - - if (shortcutKey.isNotEmpty()) - shortcutKey << ", "; - - if (key.length() == 1 && key[0] < 128) - shortcutKey << "shortcut: '" << key << '\''; - else - shortcutKey << key; + pc->addChildComponent (this); } - - item.shortcutKeyDescription = shortcutKey.trim(); - } - } - - String getTextForMeasurement() const - { - return item.shortcutKeyDescription.isNotEmpty() ? item.text + " " + item.shortcutKeyDescription - : item.text; - } - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent) -}; - -//============================================================================== -struct MenuWindow : public Component -{ - MenuWindow (const PopupMenu& menu, - MenuWindow* parentWindow, - Options opts, - bool alignToRectangle, - bool shouldDismissOnMouseUp, - ApplicationCommandManager** manager, - float parentScaleFactor = 1.0f) - : Component ("menu"), - parent (parentWindow), - options (opts.withParentComponent (findLookAndFeel (menu, parentWindow)->getParentComponentForMenuOptions (opts))), - managerOfChosenCommand (manager), - componentAttachedTo (options.getTargetComponent()), - dismissOnMouseUp (shouldDismissOnMouseUp), - windowCreationTime (Time::getMillisecondCounter()), - lastFocusedTime (windowCreationTime), - timeEnteredCurrentChildComp (windowCreationTime), - scaleFactor (parentWindow != nullptr ? parentScaleFactor : 1.0f) - { - setWantsKeyboardFocus (false); - setMouseClickGrabsKeyboardFocus (false); - setAlwaysOnTop (true); - setFocusContainerType (FocusContainerType::focusContainer); - - setLookAndFeel (findLookAndFeel (menu, parentWindow)); - - auto& lf = getLookAndFeel(); - - if (auto* pc = options.getParentComponent()) - { - pc->addChildComponent (this); - } - else - { - const auto shouldDisableAccessibility = [this] + else { - const auto* compToCheck = parent != nullptr ? parent - : options.getTargetComponent(); - - return compToCheck != nullptr && ! compToCheck->isAccessible(); - }(); - - if (shouldDisableAccessibility) - setAccessible (false); - - addToDesktop (ComponentPeer::windowIsTemporary - | ComponentPeer::windowIgnoresKeyPresses - | lf.getMenuWindowFlags()); - - Desktop::getInstance().addGlobalMouseListener (this); - } - - if (options.getParentComponent() == nullptr && parentWindow == nullptr && lf.shouldPopupMenuScaleWithTargetComponent (options)) - if (auto* targetComponent = options.getTargetComponent()) - scaleFactor = Component::getApproximateScaleFactorForComponent (targetComponent); - - setOpaque (lf.findColour (PopupMenu::backgroundColourId).isOpaque() - || ! Desktop::canUseSemiTransparentWindows()); - - const auto initialSelectedId = options.getInitiallySelectedItemId(); - - for (int i = 0; i < menu.items.size(); ++i) - { - auto& item = menu.items.getReference (i); - - if (i + 1 < menu.items.size() || ! item.isSeparator) - { - auto* child = items.add (new ItemComponent (item, options, *this)); - - if (initialSelectedId != 0 && item.itemID == initialSelectedId) - setCurrentlyHighlightedChild (child); - } - } - - auto targetArea = options.getTargetScreenArea() / scaleFactor; - - calculateWindowPos (targetArea, alignToRectangle); - setTopLeftPosition (windowPos.getPosition()); - - if (auto visibleID = options.getItemThatMustBeVisible()) - { - for (auto* item : items) - { - if (item->item.itemID == visibleID) + const auto shouldDisableAccessibility = [this] { - const auto targetPosition = [&] + const auto* compToCheck = parent != nullptr ? parent + : options.getTargetComponent(); + + return compToCheck != nullptr && ! compToCheck->isAccessible(); + }(); + + if (shouldDisableAccessibility) + setAccessible (false); + + addToDesktop (ComponentPeer::windowIsTemporary + | ComponentPeer::windowIgnoresKeyPresses + | lf.getMenuWindowFlags()); + + Desktop::getInstance().addGlobalMouseListener (this); + } + + if (options.getParentComponent() == nullptr && parentWindow == nullptr && lf.shouldPopupMenuScaleWithTargetComponent (options)) + if (auto* targetComponent = options.getTargetComponent()) + scaleFactor = Component::getApproximateScaleFactorForComponent (targetComponent); + + setOpaque (lf.findColour (PopupMenu::backgroundColourId).isOpaque() + || ! Desktop::canUseSemiTransparentWindows()); + + const auto initialSelectedId = options.getInitiallySelectedItemId(); + + for (int i = 0; i < menu.items.size(); ++i) + { + auto& item = menu.items.getReference (i); + + if (i + 1 < menu.items.size() || ! item.isSeparator) + { + auto* child = items.add (new ItemComponent (item, options, *this)); + + if (initialSelectedId != 0 && item.itemID == initialSelectedId) + setCurrentlyHighlightedChild (child); + } + } + + auto targetArea = options.getTargetScreenArea() / scaleFactor; + + calculateWindowPos (targetArea, alignToRectangle); + setTopLeftPosition (windowPos.getPosition()); + + if (auto visibleID = options.getItemThatMustBeVisible()) + { + for (auto* item : items) + { + if (item->item.itemID == visibleID) { - if (auto* pc = options.getParentComponent()) - return pc->getLocalPoint (nullptr, targetArea.getTopLeft()); + const auto targetPosition = [&] + { + if (auto* pc = options.getParentComponent()) + return pc->getLocalPoint (nullptr, targetArea.getTopLeft()); - return targetArea.getTopLeft(); - }(); + return targetArea.getTopLeft(); + }(); - auto y = targetPosition.getY() - windowPos.getY(); - ensureItemComponentIsVisible (*item, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1); + auto y = targetPosition.getY() - windowPos.getY(); + ensureItemComponentIsVisible (*item, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1); - break; + break; + } + } + } + + resizeToBestWindowPos(); + + getActiveWindows().add (this); + lf.preparePopupMenuWindow (*this); + + getMouseState (Desktop::getInstance().getMainMouseSource()); // forces creation of a mouse source watcher for the main mouse + } + + ~MenuWindow() override + { + getActiveWindows().removeFirstMatchingValue (this); + Desktop::getInstance().removeGlobalMouseListener (this); + activeSubMenu.reset(); + items.clear(); + } + + //============================================================================== + void paint (Graphics& g) override + { + if (isOpaque()) + g.fillAll (Colours::white); + + auto& theme = getLookAndFeel(); + theme.drawPopupMenuBackgroundWithOptions (g, getWidth(), getHeight(), options); + + if (columnWidths.isEmpty()) + return; + + const auto separatorWidth = theme.getPopupMenuColumnSeparatorWidthWithOptions (options); + const auto border = theme.getPopupMenuBorderSizeWithOptions (options); + + auto currentX = 0; + + std::for_each (columnWidths.begin(), std::prev (columnWidths.end()), [&] (int width) + { + const Rectangle separator (currentX + width, + border, + separatorWidth, + getHeight() - border * 2); + theme.drawPopupMenuColumnSeparatorWithOptions (g, separator, options); + currentX += width + separatorWidth; + }); + } + + void paintOverChildren (Graphics& g) override + { + auto& lf = getLookAndFeel(); + + if (options.getParentComponent()) + lf.drawResizableFrame (g, getWidth(), getHeight(), + BorderSize (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options))); + + if (canScroll()) + { + if (isTopScrollZoneActive()) + { + lf.drawPopupMenuUpDownArrowWithOptions (g, + getWidth(), + PopupMenuSettings::scrollZone, + true, + options); + } + + if (isBottomScrollZoneActive()) + { + g.setOrigin (0, getHeight() - PopupMenuSettings::scrollZone); + lf.drawPopupMenuUpDownArrowWithOptions (g, + getWidth(), + PopupMenuSettings::scrollZone, + false, + options); } } } - resizeToBestWindowPos(); - - getActiveWindows().add (this); - lf.preparePopupMenuWindow (*this); - - getMouseState (Desktop::getInstance().getMainMouseSource()); // forces creation of a mouse source watcher for the main mouse - } - - ~MenuWindow() override - { - getActiveWindows().removeFirstMatchingValue (this); - Desktop::getInstance().removeGlobalMouseListener (this); - activeSubMenu.reset(); - items.clear(); - } - - //============================================================================== - void paint (Graphics& g) override - { - if (isOpaque()) - g.fillAll (Colours::white); - - auto& theme = getLookAndFeel(); - theme.drawPopupMenuBackgroundWithOptions (g, getWidth(), getHeight(), options); - - if (columnWidths.isEmpty()) - return; - - const auto separatorWidth = theme.getPopupMenuColumnSeparatorWidthWithOptions (options); - const auto border = theme.getPopupMenuBorderSizeWithOptions (options); - - auto currentX = 0; - - std::for_each (columnWidths.begin(), std::prev (columnWidths.end()), [&] (int width) + //============================================================================== + // hide this and all sub-comps + void hide (const PopupMenu::Item* item, bool makeInvisible) { - const Rectangle separator (currentX + width, - border, - separatorWidth, - getHeight() - border * 2); - theme.drawPopupMenuColumnSeparatorWithOptions (g, separator, options); - currentX += width + separatorWidth; - }); - } - - void paintOverChildren (Graphics& g) override - { - auto& lf = getLookAndFeel(); - - if (options.getParentComponent()) - lf.drawResizableFrame (g, getWidth(), getHeight(), - BorderSize (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options))); - - if (canScroll()) - { - if (isTopScrollZoneActive()) + if (isVisible()) { - lf.drawPopupMenuUpDownArrowWithOptions (g, - getWidth(), - PopupMenuSettings::scrollZone, - true, - options); - } + WeakReference deletionChecker (this); - if (isBottomScrollZoneActive()) - { - g.setOrigin (0, getHeight() - PopupMenuSettings::scrollZone); - lf.drawPopupMenuUpDownArrowWithOptions (g, - getWidth(), - PopupMenuSettings::scrollZone, - false, - options); + activeSubMenu.reset(); + currentChild = nullptr; + + if (item != nullptr + && item->commandManager != nullptr + && item->itemID != 0) + { + *managerOfChosenCommand = item->commandManager; + } + + auto resultID = options.hasWatchedComponentBeenDeleted() ? 0 : getResultItemID (item); + + exitModalState (resultID); + exitingModalState = true; + + if (makeInvisible && deletionChecker != nullptr) + setVisible (false); + + if (resultID != 0 + && item != nullptr + && item->action != nullptr) + MessageManager::callAsync (item->action); } } - } - //============================================================================== - // hide this and all sub-comps - void hide (const PopupMenu::Item* item, bool makeInvisible) - { - if (isVisible()) + static int getResultItemID (const PopupMenu::Item* item) + { + if (item == nullptr) + return 0; + + if (auto* cc = item->customCallback.get()) + if (! cc->menuItemTriggered()) + return 0; + + return item->itemID; + } + + void dismissMenu (const PopupMenu::Item* item) + { + if (parent != nullptr) + { + parent->dismissMenu (item); + } + else + { + if (item != nullptr) + { + // need a copy of this on the stack as the one passed in will get deleted during this call + auto mi (*item); + hide (&mi, false); + } + else + { + hide (nullptr, true); + } + } + } + + float getDesktopScaleFactor() const override { return scaleFactor * Desktop::getInstance().getGlobalScaleFactor(); } + + void visibilityChanged() override + { + if (! isShowing()) + return; + + auto* accessibleFocus = [this] + { + if (currentChild != nullptr) + if (auto* childHandler = currentChild->getAccessibilityHandler()) + return childHandler; + + return getAccessibilityHandler(); + }(); + + if (accessibleFocus != nullptr) + accessibleFocus->grabFocus(); + } + + //============================================================================== + bool keyPressed (const KeyPress& key) override + { + if (key.isKeyCode (KeyPress::downKey)) + { + selectNextItem (MenuSelectionDirection::forwards); + } + else if (key.isKeyCode (KeyPress::upKey)) + { + selectNextItem (MenuSelectionDirection::backwards); + } + else if (key.isKeyCode (KeyPress::leftKey)) + { + if (parent != nullptr) + { + Component::SafePointer parentWindow (parent); + ItemComponent* currentChildOfParent = parentWindow->currentChild; + + hide (nullptr, true); + + if (parentWindow != nullptr) + parentWindow->setCurrentlyHighlightedChild (currentChildOfParent); + + disableTimerUntilMouseMoves(); + } + else if (componentAttachedTo != nullptr) + { + componentAttachedTo->keyPressed (key); + } + } + else if (key.isKeyCode (KeyPress::rightKey)) + { + disableTimerUntilMouseMoves(); + + if (showSubMenuFor (currentChild)) + { + if (isSubMenuVisible()) + activeSubMenu->selectNextItem (MenuSelectionDirection::current); + } + else if (componentAttachedTo != nullptr) + { + componentAttachedTo->keyPressed (key); + } + } + else if (key.isKeyCode (KeyPress::returnKey) || key.isKeyCode (KeyPress::spaceKey)) + { + triggerCurrentlyHighlightedItem(); + } + else if (key.isKeyCode (KeyPress::escapeKey)) + { + dismissMenu (nullptr); + } + else + { + return false; + } + + return true; + } + + void inputAttemptWhenModal() override { WeakReference deletionChecker (this); - activeSubMenu.reset(); - currentChild = nullptr; - - if (item != nullptr - && item->commandManager != nullptr - && item->itemID != 0) + for (auto* ms : mouseSourceStates) { - *managerOfChosenCommand = item->commandManager; - } + ms->timerCallback(); - auto resultID = options.hasWatchedComponentBeenDeleted() ? 0 : getResultItemID (item); - - exitModalState (resultID); - - if (deletionChecker != nullptr) - { - exitingModalState = true; - - if (makeInvisible) - setVisible (false); - } - - if (resultID != 0 - && item != nullptr - && item->action != nullptr) - MessageManager::callAsync (item->action); - } - } - - static int getResultItemID (const PopupMenu::Item* item) - { - if (item == nullptr) - return 0; - - if (auto* cc = item->customCallback.get()) - if (! cc->menuItemTriggered()) - return 0; - - return item->itemID; - } - - void dismissMenu (const PopupMenu::Item* item) - { - if (parent != nullptr) - { - parent->dismissMenu (item); - } - else - { - if (item != nullptr) - { - // need a copy of this on the stack as the one passed in will get deleted during this call - auto mi (*item); - hide (&mi, false); - } - else - { - hide (nullptr, true); - } - } - } - - float getDesktopScaleFactor() const override { return scaleFactor * Desktop::getInstance().getGlobalScaleFactor(); } - - void visibilityChanged() override - { - if (! isShowing()) - return; - - auto* accessibleFocus = [this] - { - if (currentChild != nullptr) - if (auto* childHandler = currentChild->getAccessibilityHandler()) - return childHandler; - - return getAccessibilityHandler(); - }(); - - if (accessibleFocus != nullptr) - accessibleFocus->grabFocus(); - } - - //============================================================================== - bool keyPressed (const KeyPress& key) override - { - if (key.isKeyCode (KeyPress::downKey)) - { - selectNextItem (MenuSelectionDirection::forwards); - } - else if (key.isKeyCode (KeyPress::upKey)) - { - selectNextItem (MenuSelectionDirection::backwards); - } - else if (key.isKeyCode (KeyPress::leftKey)) - { - if (parent != nullptr) - { - Component::SafePointer parentWindow (parent); - ItemComponent* currentChildOfParent = parentWindow->currentChild; - - hide (nullptr, true); - - if (parentWindow != nullptr) - parentWindow->setCurrentlyHighlightedChild (currentChildOfParent); - - disableTimerUntilMouseMoves(); - } - else if (componentAttachedTo != nullptr) - { - componentAttachedTo->keyPressed (key); - } - } - else if (key.isKeyCode (KeyPress::rightKey)) - { - disableTimerUntilMouseMoves(); - - if (showSubMenuFor (currentChild)) - { - if (isSubMenuVisible()) - activeSubMenu->selectNextItem (MenuSelectionDirection::current); - } - else if (componentAttachedTo != nullptr) - { - componentAttachedTo->keyPressed (key); - } - } - else if (key.isKeyCode (KeyPress::returnKey) || key.isKeyCode (KeyPress::spaceKey)) - { - triggerCurrentlyHighlightedItem(); - } - else if (key.isKeyCode (KeyPress::escapeKey)) - { - dismissMenu (nullptr); - } - else - { - return false; - } - - return true; - } - - void inputAttemptWhenModal() override - { - WeakReference deletionChecker (this); - - for (auto* ms : mouseSourceStates) - { - ms->timerCallback(); - - if (deletionChecker == nullptr) - return; - } - - if (! isOverAnyMenu()) - { - if (componentAttachedTo != nullptr) - { - // we want to dismiss the menu, but if we do it synchronously, then - // the mouse-click will be allowed to pass through. That's good, except - // when the user clicks on the button that originally popped the menu up, - // as they'll expect the menu to go away, and in fact it'll just - // come back. So only dismiss synchronously if they're not on the original - // comp that we're attached to. - auto mousePos = componentAttachedTo->getMouseXYRelative(); - - if (componentAttachedTo->reallyContains (mousePos, true)) - { - postCommandMessage (PopupMenuSettings::dismissCommandId); // dismiss asynchronously + if (deletionChecker == nullptr) return; - } } - dismissMenu (nullptr); + if (! isOverAnyMenu()) + { + if (componentAttachedTo != nullptr) + { + // we want to dismiss the menu, but if we do it synchronously, then + // the mouse-click will be allowed to pass through. That's good, except + // when the user clicks on the button that originally popped the menu up, + // as they'll expect the menu to go away, and in fact it'll just + // come back. So only dismiss synchronously if they're not on the original + // comp that we're attached to. + auto mousePos = componentAttachedTo->getMouseXYRelative(); + + if (componentAttachedTo->reallyContains (mousePos, true)) + { + postCommandMessage (PopupMenuSettings::dismissCommandId); // dismiss asynchronously + return; + } + } + + dismissMenu (nullptr); + } } - } - void handleCommandMessage (int commandId) override - { - Component::handleCommandMessage (commandId); - - if (commandId == PopupMenuSettings::dismissCommandId) - dismissMenu (nullptr); - } - - //============================================================================== - void mouseMove (const MouseEvent& e) override { handleMouseEvent (e); } - void mouseDown (const MouseEvent& e) override { handleMouseEvent (e); } - void mouseDrag (const MouseEvent& e) override { handleMouseEvent (e); } - void mouseUp (const MouseEvent& e) override { handleMouseEvent (e); } - - void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override - { - alterChildYPos (roundToInt (-10.0f * wheel.deltaY * PopupMenuSettings::scrollZone)); - } - - void handleMouseEvent (const MouseEvent& e) - { - getMouseState (e.source).handleMouseEvent (e); - } - - bool windowIsStillValid() - { - if (! isVisible()) - return false; - - if (componentAttachedTo != options.getTargetComponent()) + void handleCommandMessage (int commandId) override { - dismissMenu (nullptr); - return false; + Component::handleCommandMessage (commandId); + + if (commandId == PopupMenuSettings::dismissCommandId) + dismissMenu (nullptr); } - if (auto* currentlyModalWindow = dynamic_cast (Component::getCurrentlyModalComponent())) - if (! treeContains (currentlyModalWindow)) + //============================================================================== + 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 (exitingModalState) - return false; + if (componentAttachedTo != options.getTargetComponent()) + { + dismissMenu (nullptr); + return false; + } - return true; - } + if (auto* currentlyModalWindow = dynamic_cast (Component::getCurrentlyModalComponent())) + if (! treeContains (currentlyModalWindow)) + return false; - static Array& getActiveWindows() - { - static Array activeMenuWindows; - return activeMenuWindows; - } + if (exitingModalState) + return false; - MouseSourceState& getMouseState (MouseInputSource source) - { - MouseSourceState* mouseState = nullptr; - - for (auto* ms : mouseSourceStates) - { - if (ms->source == source) mouseState = ms; - else if (ms->source.getType() != source.getType()) ms->stopTimer(); - } - - if (mouseState == nullptr) - { - mouseState = new MouseSourceState (*this, source); - mouseSourceStates.add (mouseState); - } - - return *mouseState; - } - - //============================================================================== - bool isOverAnyMenu() const - { - return parent != nullptr ? parent->isOverAnyMenu() - : isOverChildren(); - } - - bool isOverChildren() const - { - return isVisible() - && (isAnyMouseOver() || (activeSubMenu != nullptr && activeSubMenu->isOverChildren())); - } - - bool isAnyMouseOver() const - { - for (auto* ms : mouseSourceStates) - if (ms->isOver()) - return true; - - return false; - } - - bool treeContains (const MenuWindow* const window) const noexcept - { - auto* mw = this; - - while (mw->parent != nullptr) - mw = mw->parent; - - while (mw != nullptr) - { - if (mw == window) - return true; - - mw = mw->activeSubMenu.get(); - } - - return false; - } - - bool doesAnyJuceCompHaveFocus() - { - if (! isForegroundOrEmbeddedProcess (componentAttachedTo)) - return false; - - if (Component::getCurrentlyFocusedComponent() != nullptr) return true; + } - for (int i = ComponentPeer::getNumPeers(); --i >= 0;) + static Array& getActiveWindows() { - if (ComponentPeer::getPeer (i)->isFocused()) + static Array activeMenuWindows; + return activeMenuWindows; + } + + MouseSourceState& getMouseState (MouseInputSource source) + { + MouseSourceState* mouseState = nullptr; + + for (auto* ms : mouseSourceStates) { - hasAnyJuceCompHadFocus = true; + if (ms->source == source) mouseState = ms; + else if (ms->source.getType() != source.getType()) ms->stopTimer(); + } + + if (mouseState == nullptr) + { + mouseState = new MouseSourceState (*this, options, source); + mouseSourceStates.add (mouseState); + } + + return *mouseState; + } + + //============================================================================== + bool isOverAnyMenu() const + { + return parent != nullptr ? parent->isOverAnyMenu() + : isOverChildren(); + } + + bool isOverChildren() const + { + return isVisible() + && (isAnyMouseOver() || (activeSubMenu != nullptr && activeSubMenu->isOverChildren())); + } + + bool isAnyMouseOver() const + { + for (auto* ms : mouseSourceStates) + if (ms->isOver()) + return true; + + return false; + } + + bool treeContains (const MenuWindow* const window) const noexcept + { + auto* mw = this; + + while (mw->parent != nullptr) + mw = mw->parent; + + while (mw != nullptr) + { + if (mw == window) + return true; + + mw = mw->activeSubMenu.get(); + } + + return false; + } + + bool doesAnyJuceCompHaveFocus() + { + if (! isForegroundOrEmbeddedProcess (componentAttachedTo)) + return false; + + if (Component::getCurrentlyFocusedComponent() != nullptr) return true; + + for (int i = ComponentPeer::getNumPeers(); --i >= 0;) + { + if (ComponentPeer::getPeer (i)->isFocused()) + { + hasAnyJuceCompHadFocus = true; + return true; + } } + + return ! hasAnyJuceCompHadFocus; } - return ! hasAnyJuceCompHadFocus; - } - - //============================================================================== - Rectangle getParentArea (Point targetPoint, Component* relativeTo = nullptr) - { - if (relativeTo != nullptr) - targetPoint = relativeTo->localPointToGlobal (targetPoint); - - auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (targetPoint * scaleFactor); - auto parentArea = display->safeAreaInsets.subtractedFrom (display->totalArea); - - if (auto* pc = options.getParentComponent()) + //============================================================================== + Rectangle getParentArea (Point targetPoint, Component* relativeTo = nullptr) { - return pc->getLocalArea (nullptr, - pc->getScreenBounds() - .reduced (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options)) - .getIntersection (parentArea)); + if (relativeTo != nullptr) + targetPoint = relativeTo->localPointToGlobal (targetPoint); + + auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (targetPoint * scaleFactor); + auto parentArea = display->safeAreaInsets.subtractedFrom (display->totalArea); + + if (auto* pc = options.getParentComponent()) + { + return pc->getLocalArea (nullptr, + pc->getScreenBounds() + .reduced (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options)) + .getIntersection (parentArea)); + } + + return parentArea; } - return parentArea; - } - - void calculateWindowPos (Rectangle target, const bool alignToRectangle) - { - auto parentArea = getParentArea (target.getCentre()) / scaleFactor; - - if (auto* pc = options.getParentComponent()) - target = pc->getLocalArea (nullptr, target).getIntersection (parentArea); - - auto maxMenuHeight = parentArea.getHeight() - 24; - - int x, y, widthToUse, heightToUse; - layoutMenuItems (parentArea.getWidth() - 24, maxMenuHeight, widthToUse, heightToUse); - - if (alignToRectangle) + void calculateWindowPos (Rectangle target, const bool alignToRectangle) { - x = target.getX(); + auto parentArea = getParentArea (target.getCentre()) / scaleFactor; - auto spaceUnder = parentArea.getBottom() - target.getBottom(); - auto spaceOver = target.getY() - parentArea.getY(); - auto bufferHeight = 30; + if (auto* pc = options.getParentComponent()) + target = pc->getLocalArea (nullptr, target).getIntersection (parentArea); - if (options.getPreferredPopupDirection() == Options::PopupDirection::upwards) - y = (heightToUse < spaceOver - bufferHeight || spaceOver >= spaceUnder) ? target.getY() - heightToUse - : target.getBottom(); + auto maxMenuHeight = parentArea.getHeight() - 24; + + int x, y, widthToUse, heightToUse; + layoutMenuItems (parentArea.getWidth() - 24, maxMenuHeight, widthToUse, heightToUse); + + if (alignToRectangle) + { + x = target.getX(); + + auto spaceUnder = parentArea.getBottom() - target.getBottom(); + auto spaceOver = target.getY() - parentArea.getY(); + auto bufferHeight = 30; + + if (options.getPreferredPopupDirection() == Options::PopupDirection::upwards) + y = (heightToUse < spaceOver - bufferHeight || spaceOver >= spaceUnder) ? target.getY() - heightToUse + : target.getBottom(); + else + y = (heightToUse < spaceUnder - bufferHeight || spaceUnder >= spaceOver) ? target.getBottom() + : target.getY() - heightToUse; + } else - y = (heightToUse < spaceUnder - bufferHeight || spaceUnder >= spaceOver) ? target.getBottom() - : target.getY() - heightToUse; - } - else - { - bool tendTowardsRight = target.getCentreX() < parentArea.getCentreX(); - - if (parent != nullptr) { - if (parent->parent != nullptr) - { - const bool parentGoingRight = (parent->getX() + parent->getWidth() / 2 - > parent->parent->getX() + parent->parent->getWidth() / 2); + bool tendTowardsRight = target.getCentreX() < parentArea.getCentreX(); - if (parentGoingRight && target.getRight() + widthToUse < parentArea.getRight() - 4) + if (parent != nullptr) + { + if (parent->parent != nullptr) + { + const bool parentGoingRight = (parent->getX() + parent->getWidth() / 2 + > parent->parent->getX() + parent->parent->getWidth() / 2); + + if (parentGoingRight && target.getRight() + widthToUse < parentArea.getRight() - 4) + tendTowardsRight = true; + else if ((! parentGoingRight) && target.getX() > widthToUse + 4) + tendTowardsRight = false; + } + else if (target.getRight() + widthToUse < parentArea.getRight() - 32) + { tendTowardsRight = true; - else if ((! parentGoingRight) && target.getX() > widthToUse + 4) - tendTowardsRight = false; + } } - else if (target.getRight() + widthToUse < parentArea.getRight() - 32) + + auto biggestSpace = jmax (parentArea.getRight() - target.getRight(), + target.getX() - parentArea.getX()) - 32; + + if (biggestSpace < widthToUse) { - tendTowardsRight = true; + layoutMenuItems (biggestSpace + target.getWidth() / 3, maxMenuHeight, widthToUse, heightToUse); + + if (numColumns > 1) + layoutMenuItems (biggestSpace - 4, maxMenuHeight, widthToUse, heightToUse); + + tendTowardsRight = (parentArea.getRight() - target.getRight()) >= (target.getX() - parentArea.getX()); + } + + x = tendTowardsRight ? jmin (parentArea.getRight() - widthToUse - 4, target.getRight()) + : jmax (parentArea.getX() + 4, target.getX() - widthToUse); + + if (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) == 0) // workaround for dismissing the window on mouse up when border size is 0 + x += tendTowardsRight ? 1 : -1; + + const auto border = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options); + y = target.getCentreY() > parentArea.getCentreY() ? jmax (parentArea.getY(), target.getBottom() - heightToUse) + border + : target.getY() - border; + } + + x = jmax (parentArea.getX() + 1, jmin (parentArea.getRight() - (widthToUse + 6), x)); + y = jmax (parentArea.getY() + 1, jmin (parentArea.getBottom() - (heightToUse + 6), y)); + + windowPos.setBounds (x, y, widthToUse, heightToUse); + + // sets this flag if it's big enough to obscure any of its parent menus + hideOnExit = parent != nullptr + && parent->windowPos.intersects (windowPos.expanded (-4, -4)); + } + + void layoutMenuItems (const int maxMenuW, const int maxMenuH, int& width, int& height) + { + // Ensure we don't try to add an empty column after the final item + if (auto* last = items.getLast()) + last->item.shouldBreakAfter = false; + + const auto isBreak = [] (const ItemComponent* item) { return item->item.shouldBreakAfter; }; + const auto numBreaks = static_cast (std::count_if (items.begin(), items.end(), isBreak)); + numColumns = numBreaks + 1; + + if (numBreaks == 0) + insertColumnBreaks (maxMenuW, maxMenuH); + + workOutManualSize (maxMenuW); + height = jmin (contentHeight, maxMenuH); + + needsToScroll = contentHeight > height; + + width = updateYPositions(); + } + + void insertColumnBreaks (const int maxMenuW, const int maxMenuH) + { + numColumns = options.getMinimumNumColumns(); + contentHeight = 0; + + auto maximumNumColumns = options.getMaximumNumColumns() > 0 ? options.getMaximumNumColumns() : 7; + + for (;;) + { + auto totalW = workOutBestSize (maxMenuW); + + if (totalW > maxMenuW) + { + numColumns = jmax (1, numColumns - 1); + workOutBestSize (maxMenuW); // to update col widths + break; + } + + if (totalW > maxMenuW / 2 + || contentHeight < maxMenuH + || numColumns >= maximumNumColumns) + break; + + ++numColumns; + } + + const auto itemsPerColumn = (items.size() + numColumns - 1) / numColumns; + + for (auto i = 0;; i += itemsPerColumn) + { + const auto breakIndex = i + itemsPerColumn - 1; + + if (breakIndex >= items.size()) + break; + + items[breakIndex]->item.shouldBreakAfter = true; + } + + if (! items.isEmpty()) + (*std::prev (items.end()))->item.shouldBreakAfter = false; + } + + int correctColumnWidths (const int maxMenuW) + { + auto totalW = std::accumulate (columnWidths.begin(), columnWidths.end(), 0); + const auto minWidth = jmin (maxMenuW, options.getMinimumWidth()); + + if (totalW < minWidth) + { + totalW = minWidth; + + for (auto& column : columnWidths) + column = totalW / numColumns; + } + + return totalW; + } + + void workOutManualSize (const int maxMenuW) + { + contentHeight = 0; + columnWidths.clear(); + + for (auto it = items.begin(), end = items.end(); it != end;) + { + const auto isBreak = [] (const ItemComponent* item) { return item->item.shouldBreakAfter; }; + const auto nextBreak = std::find_if (it, end, isBreak); + const auto columnEnd = nextBreak == end ? end : std::next (nextBreak); + + const auto getMaxWidth = [] (int acc, const ItemComponent* item) { return jmax (acc, item->getWidth()); }; + const auto colW = std::accumulate (it, columnEnd, options.getStandardItemHeight(), getMaxWidth); + const auto adjustedColW = jmin (maxMenuW / jmax (1, numColumns - 2), + colW + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2); + + const auto sumHeight = [] (int acc, const ItemComponent* item) { return acc + item->getHeight(); }; + const auto colH = std::accumulate (it, columnEnd, 0, sumHeight); + + contentHeight = jmax (contentHeight, colH); + columnWidths.add (adjustedColW); + it = columnEnd; + } + + contentHeight += getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2; + + correctColumnWidths (maxMenuW); + } + + int workOutBestSize (const int maxMenuW) + { + contentHeight = 0; + int childNum = 0; + + for (int col = 0; col < numColumns; ++col) + { + int colW = options.getStandardItemHeight(), colH = 0; + + auto numChildren = jmin (items.size() - childNum, + (items.size() + numColumns - 1) / numColumns); + + for (int i = numChildren; --i >= 0;) + { + colW = jmax (colW, items.getUnchecked (childNum + i)->getWidth()); + colH += items.getUnchecked (childNum + i)->getHeight(); + } + + colW = jmin (maxMenuW / jmax (1, numColumns - 2), + colW + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2); + + columnWidths.set (col, colW); + contentHeight = jmax (contentHeight, colH); + + childNum += numChildren; + } + + return correctColumnWidths (maxMenuW); + } + + void ensureItemComponentIsVisible (const ItemComponent& itemComp, int wantedY) + { + if (windowPos.getHeight() > PopupMenuSettings::scrollZone * 4) + { + auto currentY = itemComp.getY(); + + if (wantedY > 0 || currentY < 0 || itemComp.getBottom() > windowPos.getHeight()) + { + if (wantedY < 0) + wantedY = jlimit (PopupMenuSettings::scrollZone, + jmax (PopupMenuSettings::scrollZone, + windowPos.getHeight() - (PopupMenuSettings::scrollZone + itemComp.getHeight())), + currentY); + + auto parentArea = getParentArea (windowPos.getPosition(), options.getParentComponent()) / scaleFactor; + auto deltaY = wantedY - currentY; + + windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()), + jmin (windowPos.getHeight(), parentArea.getHeight())); + + auto newY = jlimit (parentArea.getY(), + parentArea.getBottom() - windowPos.getHeight(), + windowPos.getY() + deltaY); + + deltaY -= newY - windowPos.getY(); + + childYOffset -= deltaY; + windowPos.setPosition (windowPos.getX(), newY); + + updateYPositions(); } } + } - auto biggestSpace = jmax (parentArea.getRight() - target.getRight(), - target.getX() - parentArea.getX()) - 32; + void resizeToBestWindowPos() + { + auto r = windowPos; - if (biggestSpace < widthToUse) + if (childYOffset < 0) { - layoutMenuItems (biggestSpace + target.getWidth() / 3, maxMenuHeight, widthToUse, heightToUse); + r = r.withTop (r.getY() - childYOffset); + } + else if (childYOffset > 0) + { + auto spaceAtBottom = r.getHeight() - (contentHeight - childYOffset); - if (numColumns > 1) - layoutMenuItems (biggestSpace - 4, maxMenuHeight, widthToUse, heightToUse); - - tendTowardsRight = (parentArea.getRight() - target.getRight()) >= (target.getX() - parentArea.getX()); + if (spaceAtBottom > 0) + r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom); } - x = tendTowardsRight ? jmin (parentArea.getRight() - widthToUse - 4, target.getRight()) - : jmax (parentArea.getX() + 4, target.getX() - widthToUse); - - if (getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) == 0) // workaround for dismissing the window on mouse up when border size is 0 - x += tendTowardsRight ? 1 : -1; - - const auto border = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options); - y = target.getCentreY() > parentArea.getCentreY() ? jmax (parentArea.getY(), target.getBottom() - heightToUse) + border - : target.getY() - border; + setBounds (r); + updateYPositions(); } - x = jmax (parentArea.getX() + 1, jmin (parentArea.getRight() - (widthToUse + 6), x)); - y = jmax (parentArea.getY() + 1, jmin (parentArea.getBottom() - (heightToUse + 6), y)); - - windowPos.setBounds (x, y, widthToUse, heightToUse); - - // sets this flag if it's big enough to obscure any of its parent menus - hideOnExit = parent != nullptr - && parent->windowPos.intersects (windowPos.expanded (-4, -4)); - } - - void layoutMenuItems (const int maxMenuW, const int maxMenuH, int& width, int& height) - { - // Ensure we don't try to add an empty column after the final item - if (auto* last = items.getLast()) - last->item.shouldBreakAfter = false; - - const auto isBreak = [] (const ItemComponent* item) { return item->item.shouldBreakAfter; }; - const auto numBreaks = static_cast (std::count_if (items.begin(), items.end(), isBreak)); - numColumns = numBreaks + 1; - - if (numBreaks == 0) - insertColumnBreaks (maxMenuW, maxMenuH); - - workOutManualSize (maxMenuW); - height = jmin (contentHeight, maxMenuH); - - needsToScroll = contentHeight > height; - - width = updateYPositions(); - } - - void insertColumnBreaks (const int maxMenuW, const int maxMenuH) - { - numColumns = options.getMinimumNumColumns(); - contentHeight = 0; - - auto maximumNumColumns = options.getMaximumNumColumns() > 0 ? options.getMaximumNumColumns() : 7; - - for (;;) + void alterChildYPos (int delta) { - auto totalW = workOutBestSize (maxMenuW); - - if (totalW > maxMenuW) + if (canScroll()) { - numColumns = jmax (1, numColumns - 1); - workOutBestSize (maxMenuW); // to update col widths - break; - } + childYOffset += delta; - if (totalW > maxMenuW / 2 - || contentHeight < maxMenuH - || numColumns >= maximumNumColumns) - break; + childYOffset = [&] + { + if (delta < 0) + return jmax (childYOffset, 0); - ++numColumns; - } + if (delta > 0) + { + const auto limit = contentHeight + - windowPos.getHeight() + + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options); + return jmin (childYOffset, limit); + } - const auto itemsPerColumn = (items.size() + numColumns - 1) / numColumns; - - for (auto i = 0;; i += itemsPerColumn) - { - const auto breakIndex = i + itemsPerColumn - 1; - - if (breakIndex >= items.size()) - break; - - items[breakIndex]->item.shouldBreakAfter = true; - } - - if (! items.isEmpty()) - (*std::prev (items.end()))->item.shouldBreakAfter = false; - } - - int correctColumnWidths (const int maxMenuW) - { - auto totalW = std::accumulate (columnWidths.begin(), columnWidths.end(), 0); - const auto minWidth = jmin (maxMenuW, options.getMinimumWidth()); - - if (totalW < minWidth) - { - totalW = minWidth; - - for (auto& column : columnWidths) - column = totalW / numColumns; - } - - return totalW; - } - - void workOutManualSize (const int maxMenuW) - { - contentHeight = 0; - columnWidths.clear(); - - for (auto it = items.begin(), end = items.end(); it != end;) - { - const auto isBreak = [] (const ItemComponent* item) { return item->item.shouldBreakAfter; }; - const auto nextBreak = std::find_if (it, end, isBreak); - const auto columnEnd = nextBreak == end ? end : std::next (nextBreak); - - const auto getMaxWidth = [] (int acc, const ItemComponent* item) { return jmax (acc, item->getWidth()); }; - const auto colW = std::accumulate (it, columnEnd, options.getStandardItemHeight(), getMaxWidth); - const auto adjustedColW = jmin (maxMenuW / jmax (1, numColumns - 2), - colW + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2); - - const auto sumHeight = [] (int acc, const ItemComponent* item) { return acc + item->getHeight(); }; - const auto colH = std::accumulate (it, columnEnd, 0, sumHeight); - - contentHeight = jmax (contentHeight, colH); - columnWidths.add (adjustedColW); - it = columnEnd; - } - - contentHeight += getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2; - - correctColumnWidths (maxMenuW); - } - - int workOutBestSize (const int maxMenuW) - { - contentHeight = 0; - int childNum = 0; - - for (int col = 0; col < numColumns; ++col) - { - int colW = options.getStandardItemHeight(), colH = 0; - - auto numChildren = jmin (items.size() - childNum, - (items.size() + numColumns - 1) / numColumns); - - for (int i = numChildren; --i >= 0;) - { - colW = jmax (colW, items.getUnchecked (childNum + i)->getWidth()); - colH += items.getUnchecked (childNum + i)->getHeight(); - } - - colW = jmin (maxMenuW / jmax (1, numColumns - 2), - colW + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) * 2); - - columnWidths.set (col, colW); - contentHeight = jmax (contentHeight, colH); - - childNum += numChildren; - } - - return correctColumnWidths (maxMenuW); - } - - void ensureItemComponentIsVisible (const ItemComponent& itemComp, int wantedY) - { - if (windowPos.getHeight() > PopupMenuSettings::scrollZone * 4) - { - auto currentY = itemComp.getY(); - - if (wantedY > 0 || currentY < 0 || itemComp.getBottom() > windowPos.getHeight()) - { - if (wantedY < 0) - wantedY = jlimit (PopupMenuSettings::scrollZone, - jmax (PopupMenuSettings::scrollZone, - windowPos.getHeight() - (PopupMenuSettings::scrollZone + itemComp.getHeight())), - currentY); - - auto parentArea = getParentArea (windowPos.getPosition(), options.getParentComponent()) / scaleFactor; - auto deltaY = wantedY - currentY; - - windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()), - jmin (windowPos.getHeight(), parentArea.getHeight())); - - auto newY = jlimit (parentArea.getY(), - parentArea.getBottom() - windowPos.getHeight(), - windowPos.getY() + deltaY); - - deltaY -= newY - windowPos.getY(); - - childYOffset -= deltaY; - windowPos.setPosition (windowPos.getX(), newY); + return childYOffset; + }(); updateYPositions(); } - } - } - - void resizeToBestWindowPos() - { - auto r = windowPos; - - if (childYOffset < 0) - { - r = r.withTop (r.getY() - childYOffset); - } - else if (childYOffset > 0) - { - auto spaceAtBottom = r.getHeight() - (contentHeight - childYOffset); - - if (spaceAtBottom > 0) - r.setSize (r.getWidth(), r.getHeight() - spaceAtBottom); - } - - setBounds (r); - updateYPositions(); - } - - void alterChildYPos (int delta) - { - if (canScroll()) - { - childYOffset += delta; - - childYOffset = [&] + else { - if (delta < 0) - return jmax (childYOffset, 0); + childYOffset = 0; + } - if (delta > 0) + resizeToBestWindowPos(); + repaint(); + } + + int updateYPositions() + { + const auto separatorWidth = getLookAndFeel().getPopupMenuColumnSeparatorWidthWithOptions (options); + const auto initialY = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) + - (childYOffset + (getY() - windowPos.getY())); + + auto col = 0; + auto x = 0; + auto y = initialY; + + for (const auto& item : items) + { + jassert (col < columnWidths.size()); + const auto columnWidth = columnWidths[col]; + item->setBounds (x, y, columnWidth, item->getHeight()); + y += item->getHeight(); + + if (item->item.shouldBreakAfter) { - const auto limit = contentHeight - - windowPos.getHeight() - + getLookAndFeel().getPopupMenuBorderSizeWithOptions (options); - return jmin (childYOffset, limit); + col += 1; + x += columnWidth + separatorWidth; + y = initialY; } + } - return childYOffset; - }(); - - updateYPositions(); - } - else - { - childYOffset = 0; + return std::accumulate (columnWidths.begin(), columnWidths.end(), 0) + + (separatorWidth * (columnWidths.size() - 1)); } - resizeToBestWindowPos(); - repaint(); - } - - int updateYPositions() - { - const auto separatorWidth = getLookAndFeel().getPopupMenuColumnSeparatorWidthWithOptions (options); - const auto initialY = getLookAndFeel().getPopupMenuBorderSizeWithOptions (options) - - (childYOffset + (getY() - windowPos.getY())); - - auto col = 0; - auto x = 0; - auto y = initialY; - - for (const auto& item : items) + void setCurrentlyHighlightedChild (ItemComponent* child) { - jassert (col < columnWidths.size()); - const auto columnWidth = columnWidths[col]; - item->setBounds (x, y, columnWidth, item->getHeight()); - y += item->getHeight(); + if (currentChild != nullptr) + currentChild->setHighlighted (false); - if (item->item.shouldBreakAfter) + currentChild = child; + + if (currentChild != nullptr) { - col += 1; - x += columnWidth + separatorWidth; - y = initialY; + currentChild->setHighlighted (true); + timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter(); + } + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); + } + + bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); } + + bool showSubMenuFor (ItemComponent* childComp) + { + activeSubMenu.reset(); + + if (childComp != nullptr + && hasActiveSubMenu (childComp->item)) + { + activeSubMenu.reset (new HelperClasses::MenuWindow (*(childComp->item.subMenu), this, + options.withTargetScreenArea (childComp->getScreenBounds()) + .withMinimumWidth (0) + .withTargetComponent (nullptr), + false, dismissOnMouseUp, managerOfChosenCommand, scaleFactor)); + + activeSubMenu->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion) + activeSubMenu->enterModalState (false); + activeSubMenu->toFront (false); + return true; + } + + return false; + } + + void triggerCurrentlyHighlightedItem() + { + if (currentChild != nullptr && canBeTriggered (currentChild->item)) + { + dismissMenu (¤tChild->item); } } - return std::accumulate (columnWidths.begin(), columnWidths.end(), 0) - + (separatorWidth * (columnWidths.size() - 1)); - } - - void setCurrentlyHighlightedChild (ItemComponent* child) - { - if (currentChild != nullptr) - currentChild->setHighlighted (false); - - currentChild = child; - - if (currentChild != nullptr) + enum class MenuSelectionDirection { - currentChild->setHighlighted (true); - timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter(); + forwards, + backwards, + current + }; + + void selectNextItem (MenuSelectionDirection direction) + { + disableTimerUntilMouseMoves(); + + auto start = [&] + { + auto index = items.indexOf (currentChild); + + if (index >= 0) + return index; + + return direction == MenuSelectionDirection::backwards ? items.size() - 1 + : 0; + }(); + + auto preIncrement = (direction != MenuSelectionDirection::current && currentChild != nullptr); + + for (int i = items.size(); --i >= 0;) + { + if (preIncrement) + start += (direction == MenuSelectionDirection::backwards ? -1 : 1); + + if (auto* mic = items.getUnchecked ((start + items.size()) % items.size())) + { + if (canBeTriggered (mic->item) || hasActiveSubMenu (mic->item)) + { + setCurrentlyHighlightedChild (mic); + return; + } + } + + if (! preIncrement) + preIncrement = true; + } } - if (auto* handler = getAccessibilityHandler()) - handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); - } - - bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); } - - bool showSubMenuFor (ItemComponent* childComp) - { - activeSubMenu.reset(); - - if (childComp != nullptr - && hasActiveSubMenu (childComp->item)) + void disableTimerUntilMouseMoves() { - activeSubMenu.reset (new HelperClasses::MenuWindow (*(childComp->item.subMenu), this, - options.withTargetScreenArea (childComp->getScreenBounds()) - .withMinimumWidth (0) - .withTargetComponent (nullptr), - false, dismissOnMouseUp, managerOfChosenCommand, scaleFactor)); + disableMouseMoves = true; + + if (parent != nullptr) + parent->disableTimerUntilMouseMoves(); + } + + bool canScroll() const noexcept { return childYOffset != 0 || needsToScroll; } + bool isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; } + bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); } + + //============================================================================== + std::unique_ptr createAccessibilityHandler() override + { + return std::make_unique (*this, + AccessibilityRole::popupMenu, + AccessibilityActions().addAction (AccessibilityActionType::focus, [this] + { + if (currentChild != nullptr) + { + if (auto* handler = currentChild->getAccessibilityHandler()) + handler->grabFocus(); + } + else + { + selectNextItem (MenuSelectionDirection::forwards); + } + })); + } + + //============================================================================== + MenuWindow* parent; + const Options options; + OwnedArray items; + ApplicationCommandManager** managerOfChosenCommand; + WeakReference componentAttachedTo; + Rectangle windowPos; + bool hasBeenOver = false, needsToScroll = false; + bool dismissOnMouseUp, hideOnExit = false, disableMouseMoves = false, hasAnyJuceCompHadFocus = false; + int numColumns = 0, contentHeight = 0, childYOffset = 0; + Component::SafePointer currentChild; + std::unique_ptr activeSubMenu; + Array columnWidths; + uint32 windowCreationTime, lastFocusedTime, timeEnteredCurrentChildComp; + OwnedArray mouseSourceStates; + float scaleFactor; + bool exitingModalState = false; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow) + }; + + //============================================================================== + class MouseSourceState : public Timer + { + public: + MouseSourceState (MenuWindow& w, const Options& o, MouseInputSource s) + : window (w), options (o), source (s), lastScrollTime (Time::getMillisecondCounter()) + { + startTimerHz (20); + } + + void handleMouseEvent (const MouseEvent& e) + { + if (! window.windowIsStillValid()) + return; + + startTimerHz (20); + handleMousePosition (e.getScreenPosition()); + } + + void timerCallback() override + { +#if JUCE_WINDOWS + // touch and pen devices on Windows send an offscreen mouse move after mouse up events + // but we don't want to forward these on as they will dismiss the menu + if ((source.isTouch() || source.isPen()) && ! isValidMousePosition()) + return; +#endif + + if (window.windowIsStillValid()) + handleMousePosition (source.getScreenPosition().roundToInt()); + } + + bool isOver() const + { + return window.reallyContains (window.getLocalPoint (nullptr, source.getScreenPosition()).roundToInt(), true); + } + + MenuWindow& window; + const Options& options; + MouseInputSource source; + + private: + Point lastMousePos; + double scrollAcceleration = 0; + uint32 lastScrollTime, lastMouseMoveTime = 0; + bool isDown = false; + + void handleMousePosition (Point globalMousePos) + { + auto localMousePos = window.getLocalPoint (nullptr, globalMousePos); + auto timeNow = Time::getMillisecondCounter(); + + if (timeNow > window.timeEnteredCurrentChildComp + 100 + && window.reallyContains (localMousePos, true) + && window.currentChild != nullptr + && ! (window.disableMouseMoves || window.isSubMenuVisible())) + { + window.showSubMenuFor (window.currentChild); + } + + highlightItemUnderMouse (globalMousePos, localMousePos, timeNow); + + const bool overScrollArea = scrollIfNecessary (localMousePos, timeNow); + const bool isOverAny = window.isOverAnyMenu(); + + if (window.hideOnExit && window.hasBeenOver && ! isOverAny) + window.hide (nullptr, true); + else + checkButtonState (localMousePos, timeNow, isDown, overScrollArea, isOverAny); + } + + void checkButtonState (Point localMousePos, const uint32 timeNow, + const bool wasDown, const bool overScrollArea, const bool isOverAny) + { + isDown = window.hasBeenOver + && (ModifierKeys::currentModifiers.isAnyMouseButtonDown() + || ComponentPeer::getCurrentModifiersRealtime().isAnyMouseButtonDown()); + + if (! window.doesAnyJuceCompHaveFocus()) + { + if (timeNow > window.lastFocusedTime + 10) + { + PopupMenuSettings::menuWasHiddenBecauseOfAppChange = true; + window.dismissMenu (nullptr); + // Note: This object may have been deleted by the previous call. + } + } + else if (wasDown && timeNow > window.windowCreationTime + 250 + && ! (isDown || overScrollArea)) + { + if (window.reallyContains (localMousePos, true)) + { + auto bounds = window.getLocalBounds(); + + // Only select item if the mouse click is in the selectable area + if (localMousePos.getX() > options.getSelectableAreaLeftInset() + && localMousePos.getX() < bounds.getWidth() - options.getSelectableAreaRightInset()) + 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 globalMousePos, Point localMousePos, const uint32 timeNow) + { + if (globalMousePos != lastMousePos || timeNow > lastMouseMoveTime + 350) + { + const auto isMouseOver = window.reallyContains (localMousePos, true); + + if (isMouseOver) + window.hasBeenOver = true; + + if (lastMousePos.getDistanceFrom (globalMousePos) > 2) + { + lastMouseMoveTime = timeNow; + + if (window.disableMouseMoves && isMouseOver) + window.disableMouseMoves = false; + } + + if (window.disableMouseMoves || (window.activeSubMenu != nullptr && window.activeSubMenu->isOverChildren())) + return; + + const bool isMovingTowardsMenu = isMouseOver && globalMousePos != lastMousePos + && isMovingTowardsSubmenu (globalMousePos); + + lastMousePos = globalMousePos; + + if (! isMovingTowardsMenu) + { + auto* c = window.getComponentAt (localMousePos); + + if (c == &window) + c = nullptr; + + auto* itemUnderMouse = dynamic_cast (c); + + if (itemUnderMouse == nullptr && c != nullptr) + itemUnderMouse = c->findParentComponentOfClass(); + + if (itemUnderMouse != window.currentChild + && (isMouseOver || (window.activeSubMenu == nullptr) || ! window.activeSubMenu->isVisible())) + { + if (isMouseOver && (c != nullptr) && (window.activeSubMenu != nullptr)) + window.activeSubMenu->hide (nullptr, true); + + if (! isMouseOver) + { + if (! window.hasBeenOver) + return; + + itemUnderMouse = nullptr; + } + + window.setCurrentlyHighlightedChild (itemUnderMouse); + } + } + } + } + + bool isMovingTowardsSubmenu (Point newGlobalPos) const + { + if (window.activeSubMenu == nullptr) + return false; + + // try to intelligently guess whether the user is moving the mouse towards a currently-open + // submenu. To do this, look at whether the mouse stays inside a triangular region that + // extends from the last mouse pos to the submenu's rectangle.. + + auto itemScreenBounds = window.activeSubMenu->getScreenBounds(); + auto subX = (float) itemScreenBounds.getX(); + + auto oldGlobalPos = lastMousePos; + + if (itemScreenBounds.getX() > window.getX()) + { + oldGlobalPos -= Point (2, 0); // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels + } + else + { + oldGlobalPos += Point (2, 0); + subX += (float) itemScreenBounds.getWidth(); + } + + Path areaTowardsSubMenu; + areaTowardsSubMenu.addTriangle ((float) oldGlobalPos.x, (float) oldGlobalPos.y, + subX, (float) itemScreenBounds.getY(), + subX, (float) itemScreenBounds.getBottom()); + + return areaTowardsSubMenu.contains (newGlobalPos.toFloat()); + } + + bool scrollIfNecessary (Point 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; + } - activeSubMenu->setVisible (true); // (must be called before enterModalState on Windows to avoid DropShadower confusion) - activeSubMenu->enterModalState (false); - activeSubMenu->toFront (false); return true; } - return false; - } - - void triggerCurrentlyHighlightedItem() - { - if (currentChild != nullptr && canBeTriggered (currentChild->item)) +#if JUCE_WINDOWS + bool isValidMousePosition() { - dismissMenu (¤tChild->item); - } - } + auto screenPos = source.getScreenPosition(); + auto localPos = (window.activeSubMenu == nullptr) ? window.getLocalPoint (nullptr, screenPos) + : window.activeSubMenu->getLocalPoint (nullptr, screenPos); - enum class MenuSelectionDirection - { - forwards, - backwards, - current + if (localPos.x < 0 && localPos.y < 0) + return false; + + return true; + } +#endif + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MouseSourceState) }; - void selectNextItem (MenuSelectionDirection direction) - { - disableTimerUntilMouseMoves(); - - auto start = [&] - { - auto index = items.indexOf (currentChild); - - if (index >= 0) - return index; - - return direction == MenuSelectionDirection::backwards ? items.size() - 1 - : 0; - }(); - - auto preIncrement = (direction != MenuSelectionDirection::current && currentChild != nullptr); - - for (int i = items.size(); --i >= 0;) - { - if (preIncrement) - start += (direction == MenuSelectionDirection::backwards ? -1 : 1); - - if (auto* mic = items.getUnchecked ((start + items.size()) % items.size())) - { - if (canBeTriggered (mic->item) || hasActiveSubMenu (mic->item)) - { - setCurrentlyHighlightedChild (mic); - return; - } - } - - if (! preIncrement) - preIncrement = true; - } - } - - void disableTimerUntilMouseMoves() - { - disableMouseMoves = true; - - if (parent != nullptr) - parent->disableTimerUntilMouseMoves(); - } - - bool canScroll() const noexcept { return childYOffset != 0 || needsToScroll; } - bool isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; } - bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); } - //============================================================================== - std::unique_ptr createAccessibilityHandler() override + struct NormalComponentWrapper : public PopupMenu::CustomComponent { - return std::make_unique (*this, - AccessibilityRole::popupMenu, - AccessibilityActions().addAction (AccessibilityActionType::focus, [this] - { - if (currentChild != nullptr) - { - if (auto* handler = currentChild->getAccessibilityHandler()) - handler->grabFocus(); - } - else - { - selectNextItem (MenuSelectionDirection::forwards); - } - })); - } - - LookAndFeel* findLookAndFeel (const PopupMenu& menu, MenuWindow* parentWindow) const - { - if (parentWindow != nullptr) - return &(parentWindow->getLookAndFeel()); - - if (auto* lnf = menu.lookAndFeel.get()) - return lnf; - - return &getLookAndFeel(); - } - - //============================================================================== - MenuWindow* parent; - const Options options; - OwnedArray items; - ApplicationCommandManager** managerOfChosenCommand; - WeakReference componentAttachedTo; - Rectangle windowPos; - bool hasBeenOver = false, needsToScroll = false; - bool dismissOnMouseUp, hideOnExit = false, disableMouseMoves = false, hasAnyJuceCompHadFocus = false; - int numColumns = 0, contentHeight = 0, childYOffset = 0; - Component::SafePointer currentChild; - std::unique_ptr activeSubMenu; - Array columnWidths; - uint32 windowCreationTime, lastFocusedTime, timeEnteredCurrentChildComp; - OwnedArray mouseSourceStates; - float scaleFactor; - bool exitingModalState = false; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow) -}; - -//============================================================================== -class MouseSourceState : public Timer -{ -public: - MouseSourceState (MenuWindow& w, MouseInputSource s) - : window (w), source (s), lastScrollTime (Time::getMillisecondCounter()) - { - startTimerHz (20); - } - - void handleMouseEvent (const MouseEvent& e) - { - if (! window.windowIsStillValid()) - return; - - startTimerHz (20); - handleMousePosition (e.getScreenPosition()); - } - - void timerCallback() override - { - #if JUCE_WINDOWS - // touch and pen devices on Windows send an offscreen mouse move after mouse up events - // but we don't want to forward these on as they will dismiss the menu - if ((source.isTouch() || source.isPen()) && ! isValidMousePosition()) - return; - #endif - - if (window.windowIsStillValid()) - handleMousePosition (source.getScreenPosition().roundToInt()); - } - - bool isOver() const - { - return window.reallyContains (window.getLocalPoint (nullptr, source.getScreenPosition()).roundToInt(), true); - } - - MenuWindow& window; - MouseInputSource source; - -private: - Point lastMousePos; - double scrollAcceleration = 0; - uint32 lastScrollTime, lastMouseMoveTime = 0; - bool isDown = false; - - void handleMousePosition (Point globalMousePos) - { - auto localMousePos = window.getLocalPoint (nullptr, globalMousePos); - auto timeNow = Time::getMillisecondCounter(); - - if (timeNow > window.timeEnteredCurrentChildComp + 100 - && window.reallyContains (localMousePos, true) - && window.currentChild != nullptr - && ! (window.disableMouseMoves || window.isSubMenuVisible())) + NormalComponentWrapper (Component& comp, int w, int h, bool triggerMenuItemAutomaticallyWhenClicked) + : PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked), + width (w), height (h) { - window.showSubMenuFor (window.currentChild); + addAndMakeVisible (comp); } - 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 localMousePos, const uint32 timeNow, - const bool wasDown, const bool overScrollArea, const bool isOverAny) - { - isDown = window.hasBeenOver - && (ModifierKeys::currentModifiers.isAnyMouseButtonDown() - || ComponentPeer::getCurrentModifiersRealtime().isAnyMouseButtonDown()); - - if (! window.doesAnyJuceCompHaveFocus()) + void getIdealSize (int& idealWidth, int& idealHeight) override { - 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 globalMousePos, Point localMousePos, const uint32 timeNow) - { - if (globalMousePos != lastMousePos || timeNow > lastMouseMoveTime + 350) - { - const auto isMouseOver = window.reallyContains (localMousePos, true); - - if (isMouseOver) - window.hasBeenOver = true; - - if (lastMousePos.getDistanceFrom (globalMousePos) > 2) - { - lastMouseMoveTime = timeNow; - - if (window.disableMouseMoves && isMouseOver) - window.disableMouseMoves = false; - } - - if (window.disableMouseMoves || (window.activeSubMenu != nullptr && window.activeSubMenu->isOverChildren())) - return; - - const bool isMovingTowardsMenu = isMouseOver && globalMousePos != lastMousePos - && isMovingTowardsSubmenu (globalMousePos); - - lastMousePos = globalMousePos; - - if (! isMovingTowardsMenu) - { - auto* c = window.getComponentAt (localMousePos); - - if (c == &window) - c = nullptr; - - auto* itemUnderMouse = dynamic_cast (c); - - if (itemUnderMouse == nullptr && c != nullptr) - itemUnderMouse = c->findParentComponentOfClass(); - - if (itemUnderMouse != window.currentChild - && (isMouseOver || (window.activeSubMenu == nullptr) || ! window.activeSubMenu->isVisible())) - { - if (isMouseOver && (c != nullptr) && (window.activeSubMenu != nullptr)) - window.activeSubMenu->hide (nullptr, true); - - if (! isMouseOver) - { - if (! window.hasBeenOver) - return; - - itemUnderMouse = nullptr; - } - - window.setCurrentlyHighlightedChild (itemUnderMouse); - } - } - } - } - - bool isMovingTowardsSubmenu (Point newGlobalPos) const - { - if (window.activeSubMenu == nullptr) - return false; - - // try to intelligently guess whether the user is moving the mouse towards a currently-open - // submenu. To do this, look at whether the mouse stays inside a triangular region that - // extends from the last mouse pos to the submenu's rectangle.. - - auto itemScreenBounds = window.activeSubMenu->getScreenBounds(); - auto subX = (float) itemScreenBounds.getX(); - - auto oldGlobalPos = lastMousePos; - - if (itemScreenBounds.getX() > window.getX()) - { - oldGlobalPos -= Point (2, 0); // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels - } - else - { - oldGlobalPos += Point (2, 0); - subX += (float) itemScreenBounds.getWidth(); + idealWidth = width; + idealHeight = height; } - 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 localMousePos, const uint32 timeNow) - { - if (window.canScroll() - && isPositiveAndBelow (localMousePos.x, window.getWidth()) - && (isPositiveAndBelow (localMousePos.y, window.getHeight()) || source.isDragging())) + void resized() override { - if (window.isTopScrollZoneActive() && localMousePos.y < PopupMenuSettings::scrollZone) - return scroll (timeNow, -1); - - if (window.isBottomScrollZoneActive() && localMousePos.y > window.getHeight() - PopupMenuSettings::scrollZone) - return scroll (timeNow, 1); + if (auto* child = getChildComponent (0)) + child->setBounds (getLocalBounds()); } - scrollAcceleration = 1.0; - return false; - } + const int width, height; - bool scroll (const uint32 timeNow, const int direction) - { - if (timeNow > lastScrollTime + 20) - { - scrollAcceleration = jmin (4.0, scrollAcceleration * 1.04); - int amount = 0; - - for (int i = 0; i < window.items.size() && amount == 0; ++i) - amount = ((int) scrollAcceleration) * window.items.getUnchecked (i)->getHeight(); - - window.alterChildYPos (amount * direction); - lastScrollTime = timeNow; - } - - return true; - } - - #if JUCE_WINDOWS - bool isValidMousePosition() - { - auto screenPos = source.getScreenPosition(); - auto localPos = (window.activeSubMenu == nullptr) ? window.getLocalPoint (nullptr, screenPos) - : window.activeSubMenu->getLocalPoint (nullptr, screenPos); - - if (localPos.x < 0 && localPos.y < 0) - return false; - - return true; - } - #endif - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MouseSourceState) -}; - -//============================================================================== -struct NormalComponentWrapper : public PopupMenu::CustomComponent -{ - NormalComponentWrapper (Component& comp, int w, int h, bool triggerMenuItemAutomaticallyWhenClicked) - : PopupMenu::CustomComponent (triggerMenuItemAutomaticallyWhenClicked), - width (w), height (h) - { - addAndMakeVisible (comp); - } - - void getIdealSize (int& idealWidth, int& idealHeight) override - { - idealWidth = width; - idealHeight = height; - } - - void resized() override - { - if (auto* child = getChildComponent (0)) - child->setBounds (getLocalBounds()); - } - - const int width, height; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper) -}; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NormalComponentWrapper) + }; }; @@ -1643,21 +1657,21 @@ PopupMenu::Item::Item (Item&&) = default; PopupMenu::Item& PopupMenu::Item::operator= (Item&&) = default; PopupMenu::Item::Item (const Item& other) - : text (other.text), - itemID (other.itemID), - action (other.action), - subMenu (createCopyIfNotNull (other.subMenu.get())), - image (other.image != nullptr ? other.image->createCopy() : nullptr), - customComponent (other.customComponent), - customCallback (other.customCallback), - commandManager (other.commandManager), - shortcutKeyDescription (other.shortcutKeyDescription), - colour (other.colour), - isEnabled (other.isEnabled), - isTicked (other.isTicked), - isSeparator (other.isSeparator), - isSectionHeader (other.isSectionHeader), - shouldBreakAfter (other.shouldBreakAfter) + : text (other.text), + itemID (other.itemID), + action (other.action), + subMenu (createCopyIfNotNull (other.subMenu.get())), + image (other.image != nullptr ? other.image->createCopy() : nullptr), + customComponent (other.customComponent), + customCallback (other.customCallback), + commandManager (other.commandManager), + shortcutKeyDescription (other.shortcutKeyDescription), + colour (other.colour), + isEnabled (other.isEnabled), + isTicked (other.isTicked), + isSeparator (other.isSeparator), + isSectionHeader (other.isSectionHeader), + shouldBreakAfter (other.shouldBreakAfter) {} PopupMenu::Item& PopupMenu::Item::operator= (const Item& other) @@ -1769,8 +1783,8 @@ void PopupMenu::addItem (Item newItem) // An ID of 0 is used as a return value to indicate that the user // didn't pick anything, so you shouldn't use it as the ID for an item. jassert (newItem.itemID != 0 - || newItem.isSeparator || newItem.isSectionHeader - || newItem.subMenu != nullptr); + || newItem.isSeparator || newItem.isSectionHeader + || newItem.subMenu != nullptr); items.add (std::move (newItem)); } @@ -2040,20 +2054,19 @@ PopupMenu::Options PopupMenu::Options::withInitiallySelectedItem (int idOfItemTo return with (*this, &Options::initiallySelectedItemId, idOfItemToBeSelected); } +PopupMenu::Options PopupMenu::Options::withSelectableAreaLeftInset (int xInsetAmount) const +{ + return with (*this, &Options::selectableAreaLeftInset, xInsetAmount); +} + +PopupMenu::Options PopupMenu::Options::withSelectableAreaRightInset (int xInsetAmount) const +{ + return with (*this, &Options::selectableAreaRightInset, xInsetAmount); +} + Component* PopupMenu::createWindow (const Options& options, ApplicationCommandManager** managerOfChosenCommand) const { - #if JUCE_WINDOWS - const auto scope = [&]() -> std::unique_ptr - { - if (auto* target = options.getTargetComponent()) - if (auto* handle = target->getWindowHandle()) - return std::make_unique (handle); - - return nullptr; - }(); - #endif - return items.isEmpty() ? nullptr : new HelperClasses::MenuWindow (*this, nullptr, options, ! options.getTargetScreenArea().isEmpty(), @@ -2112,7 +2125,7 @@ struct PopupMenuCompletionCallback : public ModalComponentManager::Callback int PopupMenu::showWithOptionalCallback (const Options& options, ModalComponentManager::Callback* userCallback, - [[maybe_unused]] bool canBeModal) + bool canBeModal) { std::unique_ptr userCallbackDeleter (userCallback); std::unique_ptr callback (new PopupMenuCompletionCallback()); @@ -2128,14 +2141,15 @@ int PopupMenu::showWithOptionalCallback (const Options& options, 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.. + // be stuck behind other comps that are already modal.. - #if JUCE_MODAL_LOOPS_PERMITTED +#if JUCE_MODAL_LOOPS_PERMITTED if (userCallback == nullptr && canBeModal) return window->runModalLoop(); - #else +#else + ignoreUnused (canBeModal); jassert (! (userCallback == nullptr && canBeModal)); - #endif +#endif } return 0; @@ -2156,9 +2170,9 @@ void PopupMenu::showMenuAsync (const Options& options) void PopupMenu::showMenuAsync (const Options& options, ModalComponentManager::Callback* userCallback) { - #if ! JUCE_MODAL_LOOPS_PERMITTED +#if ! JUCE_MODAL_LOOPS_PERMITTED jassert (userCallback != nullptr); - #endif +#endif showWithOptionalCallback (options, userCallback, false); } @@ -2175,9 +2189,9 @@ int PopupMenu::show (int itemIDThatMustBeVisible, int minimumWidth, ModalComponentManager::Callback* callback) { return showWithOptionalCallback (Options().withItemThatMustBeVisible (itemIDThatMustBeVisible) - .withMinimumWidth (minimumWidth) - .withMaximumNumColumns (maximumNumColumns) - .withStandardItemHeight (standardItemHeight), + .withMinimumWidth (minimumWidth) + .withMaximumNumColumns (maximumNumColumns) + .withStandardItemHeight (standardItemHeight), callback, true); } @@ -2187,10 +2201,10 @@ int PopupMenu::showAt (Rectangle screenAreaToAttachTo, ModalComponentManager::Callback* callback) { return showWithOptionalCallback (Options().withTargetScreenArea (screenAreaToAttachTo) - .withItemThatMustBeVisible (itemIDThatMustBeVisible) - .withMinimumWidth (minimumWidth) - .withMaximumNumColumns (maximumNumColumns) - .withStandardItemHeight (standardItemHeight), + .withItemThatMustBeVisible (itemIDThatMustBeVisible) + .withMinimumWidth (minimumWidth) + .withMaximumNumColumns (maximumNumColumns) + .withStandardItemHeight (standardItemHeight), callback, true); } @@ -2200,9 +2214,9 @@ int PopupMenu::showAt (Component* componentToAttachTo, ModalComponentManager::Callback* callback) { auto options = Options().withItemThatMustBeVisible (itemIDThatMustBeVisible) - .withMinimumWidth (minimumWidth) - .withMaximumNumColumns (maximumNumColumns) - .withStandardItemHeight (standardItemHeight); + .withMinimumWidth (minimumWidth) + .withMaximumNumColumns (maximumNumColumns) + .withStandardItemHeight (standardItemHeight); if (componentToAttachTo != nullptr) options = options.withTargetComponent (componentToAttachTo); @@ -2244,7 +2258,7 @@ bool PopupMenu::containsCommandItem (const int commandID) const { for (auto& mi : items) if ((mi.itemID == commandID && mi.commandManager != nullptr) - || (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID))) + || (mi.subMenu != nullptr && mi.subMenu->containsCommandItem (commandID))) return true; return false; @@ -2357,16 +2371,6 @@ bool PopupMenu::MenuItemIterator::next() return true; } -PopupMenu::Options PopupMenu::Options::withSelectableAreaLeftInset (int xInsetAmount) const -{ - return with (*this, &Options::selectableAreaLeftInset, xInsetAmount); -} - -PopupMenu::Options PopupMenu::Options::withSelectableAreaRightInset (int xInsetAmount) const -{ - return with (*this, &Options::selectableAreaRightInset, xInsetAmount); -} - PopupMenu::Item& PopupMenu::MenuItemIterator::getItem() const { jassert (currentItem != nullptr); @@ -2392,4 +2396,4 @@ void PopupMenu::LookAndFeelMethods::getIdealPopupMenuItemSize (const String&, bo int PopupMenu::LookAndFeelMethods::getPopupMenuBorderSize() { return 0; } -} // namespace juce +} // namespace juce \ No newline at end of file diff --git a/modules/juce_gui_basics/menus/juce_PopupMenu.h b/modules/juce_gui_basics/menus/juce_PopupMenu.h index 04c2f58f1f..7b5648cb19 100644 --- a/modules/juce_gui_basics/menus/juce_PopupMenu.h +++ b/modules/juce_gui_basics/menus/juce_PopupMenu.h @@ -1,1077 +1,1101 @@ /* - ============================================================================== +============================================================================== - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited +This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited - JUCE is an open source library subject to commercial or open-source - licensing. + JUCE is an open source library subject to commercial or open-source + licensing. - By using JUCE, you agree to the terms of both the JUCE 7 End-User License - Agreement and JUCE Privacy Policy. + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). - End User License Agreement: www.juce.com/juce-7-licence - Privacy Policy: www.juce.com/juce-privacy-policy + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy - Or: You may also use this code under the terms of the GPL v3 (see - www.gnu.org/licenses). + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. - ============================================================================== -*/ + ============================================================================== + */ -namespace juce + namespace juce { + //============================================================================== + /** Creates and displays a popup-menu. -//============================================================================== -/** Creates and displays a popup-menu. + To show a popup-menu, you create one of these, add some items to it, then + call its show() method, which returns the id of the item the user selects. - To show a popup-menu, you create one of these, add some items to it, then - call its show() method, which returns the id of the item the user selects. + E.g. @code + void MyWidget::mouseDown (const MouseEvent& e) + { + PopupMenu m; + m.addItem (1, "item 1"); + m.addItem (2, "item 2"); - E.g. @code - void MyWidget::mouseDown (const MouseEvent& e) - { - PopupMenu m; - m.addItem (1, "item 1"); - m.addItem (2, "item 2"); + m.showMenuAsync (PopupMenu::Options(), + [] (int result) + { + if (result == 0) + { + // user dismissed the menu without picking anything + } + else if (result == 1) + { + // user picked item 1 + } + else if (result == 2) + { + // user picked item 2 + } + }); + } + @endcode - m.showMenuAsync (PopupMenu::Options(), - [] (int result) - { - if (result == 0) - { - // user dismissed the menu without picking anything - } - else if (result == 1) - { - // user picked item 1 - } - else if (result == 2) - { - // user picked item 2 - } - }); - } - @endcode + Submenus are easy too: @code - Submenus are easy too: @code + void MyWidget::mouseDown (const MouseEvent& e) + { + PopupMenu subMenu; + subMenu.addItem (1, "item 1"); + subMenu.addItem (2, "item 2"); - void MyWidget::mouseDown (const MouseEvent& e) - { - PopupMenu subMenu; - subMenu.addItem (1, "item 1"); - subMenu.addItem (2, "item 2"); + PopupMenu mainMenu; + mainMenu.addItem (3, "item 3"); + mainMenu.addSubMenu ("other choices", subMenu); - PopupMenu mainMenu; - mainMenu.addItem (3, "item 3"); - mainMenu.addSubMenu ("other choices", subMenu); + m.showMenuAsync (...); + } + @endcode - m.showMenuAsync (...); - } - @endcode - - @tags{GUI} + @tags{GUI} */ -class JUCE_API PopupMenu -{ -public: - //============================================================================== - /** Creates an empty popup menu. */ - PopupMenu() = default; + class JUCE_API PopupMenu + { + public: + //============================================================================== + /** Creates an empty popup menu. */ + PopupMenu() = default; - /** Creates a copy of another menu. */ - PopupMenu (const PopupMenu&); - - /** Destructor. */ - ~PopupMenu(); - - /** Copies this menu from another one. */ - PopupMenu& operator= (const PopupMenu&); - - /** Move constructor */ - PopupMenu (PopupMenu&&) noexcept; - - /** Move assignment operator */ - PopupMenu& operator= (PopupMenu&&) noexcept; - - //============================================================================== - class CustomComponent; - class CustomCallback; - - //============================================================================== - /** Resets the menu, removing all its items. */ - void clear(); - - /** Describes a popup menu item. */ - struct JUCE_API 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(); - - /** Creates an item with the given text. - This constructor also initialises the itemID to -1, which makes it suitable for - creating lambda-based item actions. - */ - Item (String text); - - Item (const Item&); - Item& operator= (const Item&); - Item (Item&&); - Item& operator= (Item&&); - - /** The menu item's name. */ - String text; - - /** The menu item's ID. - This must not be 0 if you want the item to be triggerable, but if you're attaching - an action callback to the item, you can set the itemID to -1 to indicate that it - isn't actively needed. - */ - int itemID = 0; - - /** An optional function which should be invoked when this menu item is triggered. */ - std::function action; - - /** A sub-menu, or nullptr if there isn't one. */ - std::unique_ptr subMenu; - - /** A drawable to use as an icon, or nullptr if there isn't one. */ - std::unique_ptr image; - - /** A custom component for the item to display, or nullptr if there isn't one. */ - ReferenceCountedObjectPtr customComponent; - - /** A custom callback for the item to use, or nullptr if there isn't one. */ - ReferenceCountedObjectPtr customCallback; - - /** A command manager to use to automatically invoke the command, or nullptr if none is specified. */ - ApplicationCommandManager* commandManager = nullptr; - - /** An optional string describing the shortcut key for this item. - This is only used for displaying at the right-hand edge of a menu item - the - menu won't attempt to actually catch or process the key. If you supply a - commandManager parameter then the menu will attempt to fill-in this field - automatically. - */ - String shortcutKeyDescription; - - /** 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; - - /** True if this menu item should have a tick mark next to it. */ - bool isTicked = false; - - /** True if this menu item is a separator line. */ - bool isSeparator = false; - - /** True if this menu item is a section header. */ - bool isSectionHeader = false; - - /** True if this is the final item in the current column. */ - bool shouldBreakAfter = false; - - /** Sets the isTicked flag (and returns a reference to this item to allow chaining). */ - Item& setTicked (bool shouldBeTicked = true) & noexcept; - /** Sets the isEnabled flag (and returns a reference to this item to allow chaining). */ - Item& setEnabled (bool shouldBeEnabled) & noexcept; - /** Sets the action property (and returns a reference to this item to allow chaining). */ - Item& setAction (std::function action) & noexcept; - /** Sets the itemID property (and returns a reference to this item to allow chaining). */ - Item& setID (int newID) & noexcept; - /** Sets the colour property (and returns a reference to this item to allow chaining). */ - Item& setColour (Colour) & noexcept; - /** Sets the customComponent property (and returns a reference to this item to allow chaining). */ - Item& setCustomComponent (ReferenceCountedObjectPtr customComponent) & noexcept; - /** Sets the image property (and returns a reference to this item to allow chaining). */ - Item& setImage (std::unique_ptr) & noexcept; - - /** Sets the isTicked flag (and returns a reference to this item to allow chaining). */ - Item&& setTicked (bool shouldBeTicked = true) && noexcept; - /** Sets the isEnabled flag (and returns a reference to this item to allow chaining). */ - Item&& setEnabled (bool shouldBeEnabled) && noexcept; - /** Sets the action property (and returns a reference to this item to allow chaining). */ - Item&& setAction (std::function action) && noexcept; - /** Sets the itemID property (and returns a reference to this item to allow chaining). */ - Item&& setID (int newID) && noexcept; - /** Sets the colour property (and returns a reference to this item to allow chaining). */ - Item&& setColour (Colour) && noexcept; - /** Sets the customComponent property (and returns a reference to this item to allow chaining). */ - Item&& setCustomComponent (ReferenceCountedObjectPtr customComponent) && noexcept; - /** Sets the image property (and returns a reference to this item to allow chaining). */ - Item&& setImage (std::unique_ptr) && noexcept; - }; - - /** 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 (Item newItem); - - /** Adds an item to the menu with an action callback. */ - void addItem (String itemText, - std::function action); - - /** Adds an item to the menu with an action callback. */ - void addItem (String itemText, - bool isEnabled, - bool isTicked, - std::function action); - - /** Appends a new text item for this menu to show. - - @param itemResultID the number that will be returned from the show() method - if the user picks this item. The value should never be - zero, because that's used to indicate that the user didn't - select anything. - @param itemText the text to show. - @param isEnabled if false, the item will be shown 'greyed-out' and can't be picked - @param isTicked if true, the item will be shown with a tick next to it - - @see addSeparator, addColouredItem, addCustomItem, addSubMenu - */ - void addItem (int itemResultID, - String itemText, - bool isEnabled = true, - bool isTicked = false); - - /** Appends a new item with an icon. - - @param itemResultID the number that will be returned from the show() method - if the user picks this item. The value should never be - zero, because that's used to indicate that the user didn't - select anything. - @param itemText the text to show. - @param isEnabled if false, the item will be shown 'greyed-out' and can't be picked - @param isTicked if true, the item will be shown with a tick next to it - @param iconToUse if this is a valid image, it will be displayed to the left of the item. - - @see addSeparator, addColouredItem, addCustomItem, addSubMenu - */ - void addItem (int itemResultID, - String itemText, - bool isEnabled, - bool isTicked, - const Image& iconToUse); - - /** Appends a new item with an icon. - - @param itemResultID the number that will be returned from the show() method - if the user picks this item. The value should never be - zero, because that's used to indicate that the user didn't - select anything. - @param itemText the text to show. - @param isEnabled if false, the item will be shown 'greyed-out' and can't be picked - @param isTicked if true, the item will be shown with a tick next to it - @param iconToUse a Drawable object to use as the icon to the left of the item. - The menu will take ownership of this drawable object and will - delete it later when no longer needed - @see addSeparator, addColouredItem, addCustomItem, addSubMenu - */ - void addItem (int itemResultID, - String itemText, - bool isEnabled, - bool isTicked, - std::unique_ptr iconToUse); - - /** Adds an item that represents one of the commands in a command manager object. - - @param commandManager the manager to use to trigger the command and get information - about it - @param commandID the ID of the command - @param displayName if this is non-empty, then this string will be used instead of - the command's registered name - @param iconToUse an optional Drawable object to use as the icon to the left of the item. - The menu will take ownership of this drawable object and will - delete it later when no longer needed - */ - void addCommandItem (ApplicationCommandManager* commandManager, - CommandID commandID, - String displayName = {}, - std::unique_ptr iconToUse = {}); - - /** Appends a text item with a special colour. - - This is the same as addItem(), but specifies a colour to use for the - text, which will override the default colours that are used by the - current look-and-feel. See addItem() for a description of the parameters. - */ - void addColouredItem (int itemResultID, - String itemText, - Colour itemTextColour, - bool isEnabled = true, - bool isTicked = false, - const Image& iconToUse = {}); - - /** Appends a text item with a special colour. - - This is the same as addItem(), but specifies a colour to use for the - text, which will override the default colours that are used by the - current look-and-feel. See addItem() for a description of the parameters. - */ - void addColouredItem (int itemResultID, - String itemText, - Colour itemTextColour, - bool isEnabled, - bool isTicked, - std::unique_ptr iconToUse); - - /** Appends a custom menu item. - - This will add a user-defined component to use as a menu item. - - Note that native macOS menus do not support custom components. - - itemTitle will be used as the fallback text for this item, and will - be exposed to screen reader clients. - - @see CustomComponent - */ - void addCustomItem (int itemResultID, - std::unique_ptr customComponent, - std::unique_ptr optionalSubMenu = nullptr, - const String& itemTitle = {}); - - /** Appends a custom menu item that can't be used to trigger a result. - - This will add a user-defined component to use as a menu item. - The caller must ensure that the passed-in component stays alive - until after the menu has been hidden. - - If triggerMenuItemAutomaticallyWhenClicked is true, the menu itself will handle - detection of a mouse-click on your component, and use that to trigger the - menu ID specified in itemResultID. If this is false, the menu item can't - be triggered, so itemResultID is not used. - - itemTitle will be used as the fallback text for this item, and will - be exposed to screen reader clients. - - Note that native macOS menus do not support custom components. - */ - void addCustomItem (int itemResultID, - Component& customComponent, - int idealWidth, - int idealHeight, - bool triggerMenuItemAutomaticallyWhenClicked, - std::unique_ptr optionalSubMenu = nullptr, - const String& itemTitle = {}); - - /** Appends a sub-menu. - - If the menu that's passed in is empty, it will appear as an inactive item. - If the itemResultID argument is non-zero, then the sub-menu item itself can be - clicked to trigger it as a command. - */ - void addSubMenu (String subMenuName, - PopupMenu subMenu, - bool isEnabled = true); - - /** Appends a sub-menu with an icon. - - If the menu that's passed in is empty, it will appear as an inactive item. - If the itemResultID argument is non-zero, then the sub-menu item itself can be - clicked to trigger it as a command. - */ - void addSubMenu (String subMenuName, - PopupMenu subMenu, - bool isEnabled, - const Image& iconToUse, - bool isTicked = false, - int itemResultID = 0); - - /** Appends a sub-menu with an icon. - - If the menu that's passed in is empty, it will appear as an inactive item. - If the itemResultID argument is non-zero, then the sub-menu item itself can be - clicked to trigger it as a command. - - The iconToUse parameter is a Drawable object to use as the icon to the left of - the item. The menu will take ownership of this drawable object and will delete it - later when no longer needed - */ - void addSubMenu (String subMenuName, - PopupMenu subMenu, - bool isEnabled, - std::unique_ptr iconToUse, - bool isTicked = false, - 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 multiple adjacent separators with a single - one, so your code can be quite free and easy about adding these, and it'll - always look ok. - */ - 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 (String title); - - /** Adds a column break to the menu, to help break it up into sections. - Subsequent items will be placed in a new column, rather than being appended - to the current column. - - If a menu contains explicit column breaks, the menu will never add additional - breaks. - */ - void addColumnBreak(); - - /** Returns the number of items that the menu currently contains. - (This doesn't count separators). - */ - int getNumItems() const noexcept; - - /** Returns true if the menu contains a command item that triggers the given command. */ - bool containsCommandItem (int commandID) const; - - /** Returns true if the menu contains any items that can be used. */ - bool containsAnyActiveItems() const noexcept; - - //============================================================================== - /** Class used to create a set of options to pass to the show() method. - You can chain together a series of calls to this class's methods to create - a set of whatever options you want to specify. - E.g. @code - PopupMenu menu; - ... - menu.showMenu (PopupMenu::Options().withMinimumWidth (100) - .withMaximumNumColumns (3) - .withTargetComponent (myComp)); - @endcode - */ - class JUCE_API Options - { - public: - /** By default, the target screen area will be the current mouse position. */ - Options(); - - Options (const Options&) = default; - Options& operator= (const Options&) = default; - - enum class PopupDirection - { - upwards, - downwards - }; - - //============================================================================== - /** Sets the target component to use when displaying the menu. - - This is normally the button or other control that triggered the menu. - - The target component is primarily used to control the scale of the menu, so - it's important to supply a target component if you'll be using your program - on hi-DPI displays. - - This function will also set the target screen area, so that the menu displays - next to the target component. If you need to display the menu at a specific - location, you should call withTargetScreenArea() after withTargetComponent. - - @see withTargetComponent, withTargetScreenArea - */ - [[nodiscard]] Options withTargetComponent (Component* targetComponent) const; - [[nodiscard]] Options withTargetComponent (Component& targetComponent) const; - - /** Sets the region of the screen next to which the menu should be displayed. - - To display the menu next to the mouse cursor use withMousePosition(), - which is equivalent to passing the following to this function: - @code - Rectangle{}.withPosition (Desktop::getMousePosition()) - @endcode - - withTargetComponent() will also set the target screen area. If you need - a target component and a target screen area, make sure to call - withTargetScreenArea() after withTargetComponent(). - - @see withMousePosition - */ - [[nodiscard]] Options withTargetScreenArea (Rectangle targetArea) const; - - /** Sets the target screen area to match the current mouse position. - - Make sure to call this after withTargetComponent(). - - @see withTargetScreenArea - */ - [[nodiscard]] Options withMousePosition() const; - - /** If the passed component has been deleted when the popup menu exits, - the selected item's action will not be called. - - This is useful for avoiding dangling references inside the action - callback, in the case that the callback needs to access a component that - may be deleted. - */ - [[nodiscard]] Options withDeletionCheck (Component& componentToWatchForDeletion) const; - - /** Sets the minimum width of the popup window. */ - [[nodiscard]] Options withMinimumWidth (int minWidth) const; - - /** Sets the minimum number of columns in the popup window. */ - [[nodiscard]] Options withMinimumNumColumns (int minNumColumns) const; - - /** Sets the maximum number of columns in the popup window. */ - [[nodiscard]] Options withMaximumNumColumns (int maxNumColumns) const; - - /** Sets the default height of each item in the popup menu. */ - [[nodiscard]] Options withStandardItemHeight (int standardHeight) const; - - /** Sets an item which must be visible when the menu is initially drawn. + /** Creates a copy of another menu. */ + PopupMenu (const PopupMenu&); + + /** Destructor. */ + ~PopupMenu(); + + /** Copies this menu from another one. */ + PopupMenu& operator= (const PopupMenu&); + + /** Move constructor */ + PopupMenu (PopupMenu&&) noexcept; + + /** Move assignment operator */ + PopupMenu& operator= (PopupMenu&&) noexcept; + + //============================================================================== + class CustomComponent; + class CustomCallback; + + //============================================================================== + /** Resets the menu, removing all its items. */ + void clear(); + + /** Describes a popup menu item. */ + struct JUCE_API 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(); + + /** Creates an item with the given text. + This constructor also initialises the itemID to -1, which makes it suitable for + creating lambda-based item actions. + */ + Item (String text); + + Item (const Item&); + Item& operator= (const Item&); + Item (Item&&); + Item& operator= (Item&&); + + /** The menu item's name. */ + String text; + + /** The menu item's ID. + This must not be 0 if you want the item to be triggerable, but if you're attaching + an action callback to the item, you can set the itemID to -1 to indicate that it + isn't actively needed. + */ + int itemID = 0; + + /** An optional function which should be invoked when this menu item is triggered. */ + std::function action; + + /** A sub-menu, or nullptr if there isn't one. */ + std::unique_ptr subMenu; + + /** A drawable to use as an icon, or nullptr if there isn't one. */ + std::unique_ptr image; + + /** A custom component for the item to display, or nullptr if there isn't one. */ + ReferenceCountedObjectPtr customComponent; + + /** A custom callback for the item to use, or nullptr if there isn't one. */ + ReferenceCountedObjectPtr customCallback; + + /** A command manager to use to automatically invoke the command, or nullptr if none is specified. */ + ApplicationCommandManager* commandManager = nullptr; + + /** An optional string describing the shortcut key for this item. + This is only used for displaying at the right-hand edge of a menu item - the + menu won't attempt to actually catch or process the key. If you supply a + commandManager parameter then the menu will attempt to fill-in this field + automatically. + */ + String shortcutKeyDescription; + + /** 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; + + /** True if this menu item should have a tick mark next to it. */ + bool isTicked = false; + + /** True if this menu item is a separator line. */ + bool isSeparator = false; + + /** True if this menu item is a section header. */ + bool isSectionHeader = false; + + /** True if this is the final item in the current column. */ + bool shouldBreakAfter = false; + + /** Sets the isTicked flag (and returns a reference to this item to allow chaining). */ + Item& setTicked (bool shouldBeTicked = true) & noexcept; + /** Sets the isEnabled flag (and returns a reference to this item to allow chaining). */ + Item& setEnabled (bool shouldBeEnabled) & noexcept; + /** Sets the action property (and returns a reference to this item to allow chaining). */ + Item& setAction (std::function action) & noexcept; + /** Sets the itemID property (and returns a reference to this item to allow chaining). */ + Item& setID (int newID) & noexcept; + /** Sets the colour property (and returns a reference to this item to allow chaining). */ + Item& setColour (Colour) & noexcept; + /** Sets the customComponent property (and returns a reference to this item to allow chaining). */ + Item& setCustomComponent (ReferenceCountedObjectPtr customComponent) & noexcept; + /** Sets the image property (and returns a reference to this item to allow chaining). */ + Item& setImage (std::unique_ptr) & noexcept; + + /** Sets the isTicked flag (and returns a reference to this item to allow chaining). */ + Item&& setTicked (bool shouldBeTicked = true) && noexcept; + /** Sets the isEnabled flag (and returns a reference to this item to allow chaining). */ + Item&& setEnabled (bool shouldBeEnabled) && noexcept; + /** Sets the action property (and returns a reference to this item to allow chaining). */ + Item&& setAction (std::function action) && noexcept; + /** Sets the itemID property (and returns a reference to this item to allow chaining). */ + Item&& setID (int newID) && noexcept; + /** Sets the colour property (and returns a reference to this item to allow chaining). */ + Item&& setColour (Colour) && noexcept; + /** Sets the customComponent property (and returns a reference to this item to allow chaining). */ + Item&& setCustomComponent (ReferenceCountedObjectPtr customComponent) && noexcept; + /** Sets the image property (and returns a reference to this item to allow chaining). */ + Item&& setImage (std::unique_ptr) && noexcept; + }; + + /** 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 (Item newItem); + + /** Adds an item to the menu with an action callback. */ + void addItem (String itemText, + std::function action); + + /** Adds an item to the menu with an action callback. */ + void addItem (String itemText, + bool isEnabled, + bool isTicked, + std::function action); + + /** Appends a new text item for this menu to show. + + @param itemResultID the number that will be returned from the show() method + if the user picks this item. The value should never be + zero, because that's used to indicate that the user didn't + select anything. + @param itemText the text to show. + @param isEnabled if false, the item will be shown 'greyed-out' and can't be picked + @param isTicked if true, the item will be shown with a tick next to it + + @see addSeparator, addColouredItem, addCustomItem, addSubMenu + */ + void addItem (int itemResultID, + String itemText, + bool isEnabled = true, + bool isTicked = false); + + /** Appends a new item with an icon. + + @param itemResultID the number that will be returned from the show() method + if the user picks this item. The value should never be + zero, because that's used to indicate that the user didn't + select anything. + @param itemText the text to show. + @param isEnabled if false, the item will be shown 'greyed-out' and can't be picked + @param isTicked if true, the item will be shown with a tick next to it + @param iconToUse if this is a valid image, it will be displayed to the left of the item. + + @see addSeparator, addColouredItem, addCustomItem, addSubMenu + */ + void addItem (int itemResultID, + String itemText, + bool isEnabled, + bool isTicked, + const Image& iconToUse); + + /** Appends a new item with an icon. + + @param itemResultID the number that will be returned from the show() method + if the user picks this item. The value should never be + zero, because that's used to indicate that the user didn't + select anything. + @param itemText the text to show. + @param isEnabled if false, the item will be shown 'greyed-out' and can't be picked + @param isTicked if true, the item will be shown with a tick next to it + @param iconToUse a Drawable object to use as the icon to the left of the item. + The menu will take ownership of this drawable object and will + delete it later when no longer needed + @see addSeparator, addColouredItem, addCustomItem, addSubMenu + */ + void addItem (int itemResultID, + String itemText, + bool isEnabled, + bool isTicked, + std::unique_ptr iconToUse); + + /** Adds an item that represents one of the commands in a command manager object. + + @param commandManager the manager to use to trigger the command and get information + about it + @param commandID the ID of the command + @param displayName if this is non-empty, then this string will be used instead of + the command's registered name + @param iconToUse an optional Drawable object to use as the icon to the left of the item. + The menu will take ownership of this drawable object and will + delete it later when no longer needed + */ + void addCommandItem (ApplicationCommandManager* commandManager, + CommandID commandID, + String displayName = {}, + std::unique_ptr iconToUse = {}); + + /** Appends a text item with a special colour. + + This is the same as addItem(), but specifies a colour to use for the + text, which will override the default colours that are used by the + current look-and-feel. See addItem() for a description of the parameters. + */ + void addColouredItem (int itemResultID, + String itemText, + Colour itemTextColour, + bool isEnabled = true, + bool isTicked = false, + const Image& iconToUse = {}); + + /** Appends a text item with a special colour. + + This is the same as addItem(), but specifies a colour to use for the + text, which will override the default colours that are used by the + current look-and-feel. See addItem() for a description of the parameters. + */ + void addColouredItem (int itemResultID, + String itemText, + Colour itemTextColour, + bool isEnabled, + bool isTicked, + std::unique_ptr iconToUse); + + /** Appends a custom menu item. + + This will add a user-defined component to use as a menu item. + + Note that native macOS menus do not support custom components. + + itemTitle will be used as the fallback text for this item, and will + be exposed to screen reader clients. + + @see CustomComponent + */ + void addCustomItem (int itemResultID, + std::unique_ptr customComponent, + std::unique_ptr optionalSubMenu = nullptr, + const String& itemTitle = {}); + + /** Appends a custom menu item that can't be used to trigger a result. + + This will add a user-defined component to use as a menu item. + The caller must ensure that the passed-in component stays alive + until after the menu has been hidden. + + If triggerMenuItemAutomaticallyWhenClicked is true, the menu itself will handle + detection of a mouse-click on your component, and use that to trigger the + menu ID specified in itemResultID. If this is false, the menu item can't + be triggered, so itemResultID is not used. + + itemTitle will be used as the fallback text for this item, and will + be exposed to screen reader clients. + + Note that native macOS menus do not support custom components. + */ + void addCustomItem (int itemResultID, + Component& customComponent, + int idealWidth, + int idealHeight, + bool triggerMenuItemAutomaticallyWhenClicked, + std::unique_ptr optionalSubMenu = nullptr, + const String& itemTitle = {}); + + /** Appends a sub-menu. + + If the menu that's passed in is empty, it will appear as an inactive item. + If the itemResultID argument is non-zero, then the sub-menu item itself can be + clicked to trigger it as a command. + */ + void addSubMenu (String subMenuName, + PopupMenu subMenu, + bool isEnabled = true); + + /** Appends a sub-menu with an icon. + + If the menu that's passed in is empty, it will appear as an inactive item. + If the itemResultID argument is non-zero, then the sub-menu item itself can be + clicked to trigger it as a command. + */ + void addSubMenu (String subMenuName, + PopupMenu subMenu, + bool isEnabled, + const Image& iconToUse, + bool isTicked = false, + int itemResultID = 0); + + /** Appends a sub-menu with an icon. + + If the menu that's passed in is empty, it will appear as an inactive item. + If the itemResultID argument is non-zero, then the sub-menu item itself can be + clicked to trigger it as a command. + + The iconToUse parameter is a Drawable object to use as the icon to the left of + the item. The menu will take ownership of this drawable object and will delete it + later when no longer needed + */ + void addSubMenu (String subMenuName, + PopupMenu subMenu, + bool isEnabled, + std::unique_ptr iconToUse, + bool isTicked = false, + 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 multiple adjacent separators with a single + one, so your code can be quite free and easy about adding these, and it'll + always look ok. + */ + 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 (String title); + + /** Adds a column break to the menu, to help break it up into sections. + Subsequent items will be placed in a new column, rather than being appended + to the current column. + + If a menu contains explicit column breaks, the menu will never add additional + breaks. + */ + void addColumnBreak(); + + /** Returns the number of items that the menu currently contains. + (This doesn't count separators). + */ + int getNumItems() const noexcept; + + /** Returns true if the menu contains a command item that triggers the given command. */ + bool containsCommandItem (int commandID) const; + + /** Returns true if the menu contains any items that can be used. */ + bool containsAnyActiveItems() const noexcept; + + //============================================================================== + /** Class used to create a set of options to pass to the show() method. + You can chain together a series of calls to this class's methods to create + a set of whatever options you want to specify. + E.g. @code + PopupMenu menu; + ... + menu.showMenu (PopupMenu::Options().withMinimumWidth (100) + .withMaximumNumColumns (3) + .withTargetComponent (myComp)); + @endcode + */ + class JUCE_API Options + { + public: + /** By default, the target screen area will be the current mouse position. */ + Options(); + + Options (const Options&) = default; + Options& operator= (const Options&) = default; + + enum class PopupDirection + { + upwards, + downwards + }; + + //============================================================================== + /** Sets the target component to use when displaying the menu. + + This is normally the button or other control that triggered the menu. + + The target component is primarily used to control the scale of the menu, so + it's important to supply a target component if you'll be using your program + on hi-DPI displays. + + This function will also set the target screen area, so that the menu displays + next to the target component. If you need to display the menu at a specific + location, you should call withTargetScreenArea() after withTargetComponent. + + @see withTargetComponent, withTargetScreenArea + */ + JUCE_NODISCARD Options withTargetComponent (Component* targetComponent) const; + JUCE_NODISCARD Options withTargetComponent (Component& targetComponent) const; + + /** Sets the region of the screen next to which the menu should be displayed. + + To display the menu next to the mouse cursor use withMousePosition(), + which is equivalent to passing the following to this function: + @code + Rectangle{}.withPosition (Desktop::getMousePosition()) + @endcode + + withTargetComponent() will also set the target screen area. If you need + a target component and a target screen area, make sure to call + withTargetScreenArea() after withTargetComponent(). + + @see withMousePosition + */ + JUCE_NODISCARD Options withTargetScreenArea (Rectangle targetArea) const; + + /** Sets the target screen area to match the current mouse position. + + Make sure to call this after withTargetComponent(). + + @see withTargetScreenArea + */ + JUCE_NODISCARD Options withMousePosition() const; + + /** If the passed component has been deleted when the popup menu exits, + the selected item's action will not be called. - This is useful to ensure that a particular item is shown when the menu - contains too many items to display on a single screen. - */ - [[nodiscard]] Options withItemThatMustBeVisible (int idOfItemToBeVisible) const; + This is useful for avoiding dangling references inside the action + callback, in the case that the callback needs to access a component that + may be deleted. + */ + JUCE_NODISCARD Options withDeletionCheck (Component& componentToWatchForDeletion) const; - /** Sets a component that the popup menu will be drawn into. + /** Sets the minimum width of the popup window. */ + JUCE_NODISCARD Options withMinimumWidth (int minWidth) const; - Some plugin formats, such as AUv3, dislike it when the plugin editor - spawns additional windows. Some AUv3 hosts display pink backgrounds - underneath transparent popup windows, which is confusing and can appear - as though the plugin is malfunctioning. Setting a parent component will - avoid this unwanted behaviour, but with the downside that the menu size - will be constrained by the size of the parent component. - */ - [[nodiscard]] Options withParentComponent (Component* parentComponent) const; + /** Sets the minimum number of columns in the popup window. */ + JUCE_NODISCARD Options withMinimumNumColumns (int minNumColumns) const; - /** Sets the direction of the popup menu relative to the target screen area. */ - [[nodiscard]] Options withPreferredPopupDirection (PopupDirection direction) const; - - - /** Provide an X value from the left edge of any PopupMenu item such - that clicks to the left of the X value will NOT select the Popup - menu item, but clicks to the right will select the Popup men item. + /** Sets the maximum number of columns in the popup window. */ + JUCE_NODISCARD Options withMaximumNumColumns (int maxNumColumns) const; - This is useful for blocking off area for extra UI in a - PopupMenu::CustomComponent that you do not want to be used for - selecting a menu item. + /** Sets the default height of each item in the popup menu. */ + JUCE_NODISCARD Options withStandardItemHeight (int standardHeight) const; - @note Added by Tim for FAW SampleComboBox.h so that we could prevent - the sample audio preview buttons in the SamplePopMenuItem - from selecting the item. - */ - [[nodiscard]] Options withSelectableAreaLeftInset (int xInsetAmount) const; + /** Sets an item which must be visible when the menu is initially drawn. + This is useful to ensure that a particular item is shown when the menu + contains too many items to display on a single screen. + */ + JUCE_NODISCARD Options withItemThatMustBeVisible (int idOfItemToBeVisible) const; - /** Provide an X value from the right edge of any PopupMenu item such - that clicks to the right of the X value will NOT select the Popup - menu item, but clicks to the left will select the Popup men item. + /** Sets a component that the popup menu will be drawn into. - This is useful for blocking off area for extra UI in a - PopupMenu::CustomComponent that you do not want to be used for - selecting a menu item. + Some plugin formats, such as AUv3, dislike it when the plugin editor + spawns additional windows. Some AUv3 hosts display pink backgrounds + underneath transparent popup windows, which is confusing and can appear + as though the plugin is malfunctioning. Setting a parent component will + avoid this unwanted behaviour, but with the downside that the menu size + will be constrained by the size of the parent component. + */ + JUCE_NODISCARD Options withParentComponent (Component* parentComponent) const; - @note Added by Tim for FAW SampleComboBox.h so that we could prevent - the favorite buttons in the SamplePopMenuItem from selecting - the item. - */ - [[nodiscard]] Options withSelectableAreaRightInset (int xInsetAmount) const; - + /** Sets the direction of the popup menu relative to the target screen area. */ + JUCE_NODISCARD Options withPreferredPopupDirection (PopupDirection direction) const; - /** Sets an item to select in the menu. + /** Sets an item to select in the menu. - This is useful for controls such as combo boxes, where opening the combo box - with the keyboard should ideally highlight the currently-selected item, allowing - the next/previous item to be selected by pressing up/down on the keyboard, rather - than needing to move the highlighted row down from the top of the menu each time - it is opened. - */ - [[nodiscard]] Options withInitiallySelectedItem (int idOfItemToBeSelected) const; + This is useful for controls such as combo boxes, where opening the combo box + with the keyboard should ideally highlight the currently-selected item, allowing + the next/previous item to be selected by pressing up/down on the keyboard, rather + than needing to move the highlighted row down from the top of the menu each time + it is opened. + */ + JUCE_NODISCARD Options withInitiallySelectedItem (int idOfItemToBeSelected) const; - //============================================================================== - /** Gets the parent component. This may be nullptr if the Component has been deleted. - @see withParentComponent - */ - Component* getParentComponent() const noexcept { return parentComponent; } - - /** Gets the target component. This may be nullptr if the Component has been deleted. - - @see withTargetComponent - */ - Component* getTargetComponent() const noexcept { return targetComponent; } - - /** Returns true if the menu was watching a component, and that component has been deleted, and false otherwise. - - @see withDeletionCheck - */ - bool hasWatchedComponentBeenDeleted() const noexcept { return isWatchingForDeletion && componentToWatchForDeletion == nullptr; } - - /** Gets the target screen area. - - @see withTargetScreenArea - */ - Rectangle getTargetScreenArea() const noexcept { return targetArea; } - - /** Gets the minimum width. - - @see withMinimumWidth - */ - int getMinimumWidth() const noexcept { return minWidth; } - - /** Gets the maximum number of columns. - - @see withMaximumNumColumns - */ - int getMaximumNumColumns() const noexcept { return maxColumns; } - - /** Gets the minimum number of columns. - - @see withMinimumNumColumns - */ - int getMinimumNumColumns() const noexcept { return minColumns; } - - /** Gets the default height of items in the menu. - - @see withStandardItemHeight - */ - int getStandardItemHeight() const noexcept { return standardHeight; } - - /** Gets the ID of the item that must be visible when the menu is initially shown. - - @see withItemThatMustBeVisible - */ - int getItemThatMustBeVisible() const noexcept { return visibleItemID; } - - /** Gets the preferred popup menu direction. - - @see withPreferredPopupDirection - */ - PopupDirection getPreferredPopupDirection() const noexcept { return preferredPopupDirection; } - - /** Gets the ID of the item that must be selected when the menu is initially shown. - - @see withItemThatMustBeVisible - */ - int getInitiallySelectedItemId() const noexcept { return initiallySelectedItemId; } - - private: - //============================================================================== - Rectangle targetArea; - WeakReference targetComponent, parentComponent, componentToWatchForDeletion; - int visibleItemID = 0, minWidth = 0, minColumns = 1, maxColumns = 0, standardHeight = 0, initiallySelectedItemId = 0; - bool isWatchingForDeletion = false; - PopupDirection preferredPopupDirection = PopupDirection::downwards; - }; - - //============================================================================== - #if JUCE_MODAL_LOOPS_PERMITTED - /** Displays the menu and waits for the user to pick something. - - This will display the menu modally, and return the ID of the item that the - user picks. If they click somewhere off the menu to get rid of it without - choosing anything, this will return 0. - - The current location of the mouse will be used as the position to show the - menu - to explicitly set the menu's position, use showAt() instead. Depending - on where this point is on the screen, the menu will appear above, below or - to the side of the point. - - @param itemIDThatMustBeVisible if you set this to the ID of one of the menu items, - then when the menu first appears, it will make sure - that this item is visible. So if the menu has too many - items to fit on the screen, it will be scrolled to a - position where this item is visible. - @param minimumWidth a minimum width for the menu, in pixels. It may be wider - than this if some items are too long to fit. - @param maximumNumColumns if there are too many items to fit on-screen in a single - vertical column, the menu may be laid out as a series of - columns - this is the maximum number allowed. To use the - default value for this (probably about 7), you can pass - in zero. - @param standardItemHeight if this is non-zero, it will be used as the standard - height for menu items (apart from custom items) - @param callback if this is not a nullptr, the menu will be launched - asynchronously, returning immediately, and the callback - will receive a call when the menu is either dismissed or - has an item selected. This object will be owned and - deleted by the system, so make sure that it works safely - and that any pointers that it uses are safely within scope. - @see showAt - */ - int show (int itemIDThatMustBeVisible = 0, - int minimumWidth = 0, - int maximumNumColumns = 0, - int standardItemHeight = 0, - ModalComponentManager::Callback* callback = nullptr); - - - /** Displays the menu at a specific location. - - This is the same as show(), but uses a specific location (in global screen - coordinates) rather than the current mouse position. - - The screenAreaToAttachTo parameter indicates a screen area to which the menu - will be adjacent. Depending on where this is, the menu will decide which edge to - attach itself to, in order to fit itself fully on-screen. If you just want to - trigger a menu at a specific point, you can pass in a rectangle of size (0, 0) - with the position that you want. - - @see show() - */ - int showAt (Rectangle screenAreaToAttachTo, - int itemIDThatMustBeVisible = 0, + /** Provide an X value from the left edge of any PopupMenu item such + that clicks to the left of the X value will NOT select the Popup + menu item, but clicks to the right will select the Popup men item. + + This is useful for blocking off area for extra UI in a + PopupMenu::CustomComponent that you do not want to be used for + selecting a menu item. + + @note Added by Tim for FAW SampleComboBox.h so that we could prevent + the sample audio preview buttons in the SamplePopMenuItem + from selecting the item. + */ + JUCE_NODISCARD Options withSelectableAreaLeftInset (int xInsetAmount) const; + + + /** Provide an X value from the right edge of any PopupMenu item such + that clicks to the right of the X value will NOT select the Popup + menu item, but clicks to the left will select the Popup men item. + + This is useful for blocking off area for extra UI in a + PopupMenu::CustomComponent that you do not want to be used for + selecting a menu item. + + @note Added by Tim for FAW SampleComboBox.h so that we could prevent + the favorite buttons in the SamplePopMenuItem from selecting + the item. + */ + JUCE_NODISCARD Options withSelectableAreaRightInset (int xInsetAmount) const; + + //============================================================================== + /** Gets the parent component. This may be nullptr if the Component has been deleted. + + @see withParentComponent + */ + Component* getParentComponent() const noexcept { return parentComponent; } + + /** Gets the target component. This may be nullptr if the Component has been deleted. + + @see withTargetComponent + */ + Component* getTargetComponent() const noexcept { return targetComponent; } + + /** Returns true if the menu was watching a component, and that component has been deleted, and false otherwise. + + @see withDeletionCheck + */ + bool hasWatchedComponentBeenDeleted() const noexcept { return isWatchingForDeletion && componentToWatchForDeletion == nullptr; } + + /** Gets the target screen area. + + @see withTargetScreenArea + */ + Rectangle getTargetScreenArea() const noexcept { return targetArea; } + + /** Gets the minimum width. + + @see withMinimumWidth + */ + int getMinimumWidth() const noexcept { return minWidth; } + + /** Gets the maximum number of columns. + + @see withMaximumNumColumns + */ + int getMaximumNumColumns() const noexcept { return maxColumns; } + + /** Gets the minimum number of columns. + + @see withMinimumNumColumns + */ + int getMinimumNumColumns() const noexcept { return minColumns; } + + /** Gets the default height of items in the menu. + + @see withStandardItemHeight + */ + int getStandardItemHeight() const noexcept { return standardHeight; } + + /** Gets the ID of the item that must be visible when the menu is initially shown. + + @see withItemThatMustBeVisible + */ + int getItemThatMustBeVisible() const noexcept { return visibleItemID; } + + /** Gets the preferred popup menu direction. + + @see withPreferredPopupDirection + */ + PopupDirection getPreferredPopupDirection() const noexcept { return preferredPopupDirection; } + + /** Gets the ID of the item that must be selected when the menu is initially shown. + + @see withItemThatMustBeVisible + */ + int getInitiallySelectedItemId() const noexcept { return initiallySelectedItemId; } + + /** Gets the left inset (x) amount used to determine mouse clicks that + select PopupMenu items. A click with an X less than this value will + not select the PopupMenu item. + + @see withItemThatMustBeVisible + + @note Added by Tim for FAW SampleComboBox.h so that we could prevent + the sample audio preview buttons in the SamplePopMenuItem + from selecting the item. + */ + int getSelectableAreaLeftInset() const noexcept { return selectableAreaLeftInset; } + + /** Gets the right inset (x) amount used to determine mouse clicks that + select PopupMenu items. A click with an X greater than this value will + not select the PopupMenu item. + + @see withItemThatMustBeVisible + + @note Added by Tim for FAW SampleComboBox.h so that we could prevent + the sample favorite buttons in the SamplePopMenuItem + from selecting the item. + */ + int getSelectableAreaRightInset() const noexcept { return selectableAreaRightInset; } + + private: + //============================================================================== + Rectangle targetArea; + WeakReference targetComponent, parentComponent, componentToWatchForDeletion; + int visibleItemID = 0, minWidth = 0, minColumns = 1, maxColumns = 0, standardHeight = 0, initiallySelectedItemId = 0; + bool isWatchingForDeletion = false; + PopupDirection preferredPopupDirection = PopupDirection::downwards; + int selectableAreaLeftInset = 0; + int selectableAreaRightInset = 0; + }; + + //============================================================================== +#if JUCE_MODAL_LOOPS_PERMITTED + /** Displays the menu and waits for the user to pick something. + + This will display the menu modally, and return the ID of the item that the + user picks. If they click somewhere off the menu to get rid of it without + choosing anything, this will return 0. + + The current location of the mouse will be used as the position to show the + menu - to explicitly set the menu's position, use showAt() instead. Depending + on where this point is on the screen, the menu will appear above, below or + to the side of the point. + + @param itemIDThatMustBeVisible if you set this to the ID of one of the menu items, + then when the menu first appears, it will make sure + that this item is visible. So if the menu has too many + items to fit on the screen, it will be scrolled to a + position where this item is visible. + @param minimumWidth a minimum width for the menu, in pixels. It may be wider + than this if some items are too long to fit. + @param maximumNumColumns if there are too many items to fit on-screen in a single + vertical column, the menu may be laid out as a series of + columns - this is the maximum number allowed. To use the + default value for this (probably about 7), you can pass + in zero. + @param standardItemHeight if this is non-zero, it will be used as the standard + height for menu items (apart from custom items) + @param callback if this is not a nullptr, the menu will be launched + asynchronously, returning immediately, and the callback + will receive a call when the menu is either dismissed or + has an item selected. This object will be owned and + deleted by the system, so make sure that it works safely + and that any pointers that it uses are safely within scope. + @see showAt + */ + int show (int itemIDThatMustBeVisible = 0, int minimumWidth = 0, int maximumNumColumns = 0, int standardItemHeight = 0, ModalComponentManager::Callback* callback = nullptr); - /** Displays the menu as if it's attached to a component such as a button. - This is similar to showAt(), but will position it next to the given component, e.g. - so that the menu's edge is aligned with that of the component. This is intended for - things like buttons that trigger a pop-up menu. - */ - int showAt (Component* componentToAttachTo, - int itemIDThatMustBeVisible = 0, - int minimumWidth = 0, - int maximumNumColumns = 0, - int standardItemHeight = 0, - ModalComponentManager::Callback* callback = nullptr); + /** Displays the menu at a specific location. - /** Displays and runs the menu modally, with a set of options. - */ - int showMenu (const Options& options); - #endif + This is the same as show(), but uses a specific location (in global screen + coordinates) rather than the current mouse position. - /** Runs the menu asynchronously. */ - void showMenuAsync (const Options& options); + The screenAreaToAttachTo parameter indicates a screen area to which the menu + will be adjacent. Depending on where this is, the menu will decide which edge to + attach itself to, in order to fit itself fully on-screen. If you just want to + trigger a menu at a specific point, you can pass in a rectangle of size (0, 0) + with the position that you want. - /** Runs the menu asynchronously, with a user-provided callback that will receive the result. */ - void showMenuAsync (const Options& options, - ModalComponentManager::Callback* callback); + @see show() + */ + int showAt (Rectangle screenAreaToAttachTo, + int itemIDThatMustBeVisible = 0, + int minimumWidth = 0, + int maximumNumColumns = 0, + int standardItemHeight = 0, + ModalComponentManager::Callback* callback = nullptr); - /** Runs the menu asynchronously, with a user-provided callback that will receive the result. */ - void showMenuAsync (const Options& options, - std::function callback); + /** Displays the menu as if it's attached to a component such as a button. - //============================================================================== - /** Closes any menus that are currently open. + This is similar to showAt(), but will position it next to the given component, e.g. + so that the menu's edge is aligned with that of the component. This is intended for + things like buttons that trigger a pop-up menu. + */ + int showAt (Component* componentToAttachTo, + int itemIDThatMustBeVisible = 0, + int minimumWidth = 0, + int maximumNumColumns = 0, + int standardItemHeight = 0, + ModalComponentManager::Callback* callback = nullptr); - This might be useful if you have a situation where your window is being closed - by some means other than a user action, and you'd like to make sure that menus - aren't left hanging around. - */ - static bool JUCE_CALLTYPE dismissAllActiveMenus(); + /** Displays and runs the menu modally, with a set of options. + */ + int showMenu (const Options& options); +#endif + + /** Runs the menu asynchronously. */ + void showMenuAsync (const Options& options); + + /** Runs the menu asynchronously, with a user-provided callback that will receive the result. */ + void showMenuAsync (const Options& options, + ModalComponentManager::Callback* callback); + + /** Runs the menu asynchronously, with a user-provided callback that will receive the result. */ + void showMenuAsync (const Options& options, + std::function callback); + + //============================================================================== + /** Closes any menus that are currently open. + + This might be useful if you have a situation where your window is being closed + by some means other than a user action, and you'd like to make sure that menus + aren't left hanging around. + */ + static bool JUCE_CALLTYPE dismissAllActiveMenus(); - //============================================================================== - /** Specifies a look-and-feel for the menu and any sub-menus that it has. + //============================================================================== + /** Specifies a look-and-feel for the menu and any sub-menus that it has. - This can be called before show() if you need a customised menu. Be careful - not to delete the LookAndFeel object before the menu has been deleted. - */ - void setLookAndFeel (LookAndFeel* newLookAndFeel); + This can be called before show() if you need a customised menu. Be careful + not to delete the LookAndFeel object before the menu has been deleted. + */ + void setLookAndFeel (LookAndFeel* newLookAndFeel); - //============================================================================== - /** A set of colour IDs to use to change the colour of various aspects of the menu. + //============================================================================== + /** A set of colour IDs to use to change the colour of various aspects of the menu. - These constants can be used either via the LookAndFeel::setColour() - method for the look and feel that is set for this menu with setLookAndFeel() + These constants can be used either via the LookAndFeel::setColour() + method for the look and feel that is set for this menu with setLookAndFeel() - @see setLookAndFeel, LookAndFeel::setColour, LookAndFeel::findColour - */ - enum ColourIds - { - backgroundColourId = 0x1000700, /**< The colour to fill the menu's background with. */ - textColourId = 0x1000600, /**< The colour for normal menu item text, (unless the - colour is specified when the item is added). */ - headerTextColourId = 0x1000601, /**< The colour for section header item text (see the - addSectionHeader() method). */ - highlightedBackgroundColourId = 0x1000900, /**< The colour to fill the background of the currently - highlighted menu item. */ - highlightedTextColourId = 0x1000800, /**< The colour to use for the text of the currently - highlighted item. */ - }; + @see setLookAndFeel, LookAndFeel::setColour, LookAndFeel::findColour + */ + enum ColourIds + { + backgroundColourId = 0x1000700, /**< The colour to fill the menu's background with. */ + textColourId = 0x1000600, /**< The colour for normal menu item text, (unless the + colour is specified when the item is added). */ + headerTextColourId = 0x1000601, /**< The colour for section header item text (see the + addSectionHeader() method). */ + highlightedBackgroundColourId = 0x1000900, /**< The colour to fill the background of the currently + highlighted menu item. */ + highlightedTextColourId = 0x1000800, /**< The colour to use for the text of the currently + highlighted item. */ + }; - //============================================================================== - /** - Allows you to iterate through the items in a pop-up menu, and examine - their properties. + //============================================================================== + /** + Allows you to iterate through the items in a pop-up menu, and examine + their properties. - To use this, just create one and repeatedly call its next() method. When this - returns true, all the member variables of the iterator are filled-out with - information describing the menu item. When it returns false, the end of the - list has been reached. - */ - class JUCE_API MenuItemIterator - { - public: - //============================================================================== - /** Creates an iterator that will scan through the items in the specified - menu. + To use this, just create one and repeatedly call its next() method. When this + returns true, all the member variables of the iterator are filled-out with + information describing the menu item. When it returns false, the end of the + list has been reached. + */ + class JUCE_API MenuItemIterator + { + public: + //============================================================================== + /** Creates an iterator that will scan through the items in the specified + menu. - Be careful not to add any items to a menu while it is being iterated, - or things could get out of step. + Be careful not to add any items to a menu while it is being iterated, + or things could get out of step. - @param menu the menu that needs to be scanned + @param menu the menu that needs to be scanned - @param searchRecursively if true, all submenus will be recursed into to - do an exhaustive search - */ - MenuItemIterator (const PopupMenu& menu, bool searchRecursively = false); + @param searchRecursively if true, all submenus will be recursed into to + do an exhaustive search + */ + MenuItemIterator (const PopupMenu& menu, bool searchRecursively = false); - /** Destructor. */ - ~MenuItemIterator(); + /** Destructor. */ + ~MenuItemIterator(); - /** Returns true if there is another item, and sets up all this object's - member variables to reflect that item's properties. - */ - bool next(); + /** Returns true if there is another item, and sets up all this object's + member variables to reflect that item's properties. + */ + bool next(); - /** Returns a reference to the description of the current item. - It is only valid to call this after next() has returned true! - */ - Item& getItem() const; + /** Returns a reference to the description of the current item. + It is only valid to call this after next() has returned true! + */ + Item& getItem() const; - private: - //============================================================================== - bool searchRecursively; + private: + //============================================================================== + bool searchRecursively; - Array index; - Array menus; - PopupMenu::Item* currentItem = nullptr; + Array index; + Array menus; + PopupMenu::Item* currentItem = nullptr; - MenuItemIterator& operator= (const MenuItemIterator&); - JUCE_LEAK_DETECTOR (MenuItemIterator) - }; + MenuItemIterator& operator= (const MenuItemIterator&); + JUCE_LEAK_DETECTOR (MenuItemIterator) + }; - //============================================================================== - /** A user-defined component that can be used as an item in a popup menu. - @see PopupMenu::addCustomItem - */ - class JUCE_API CustomComponent : public Component, + //============================================================================== + /** A user-defined component that can be used as an item in a popup menu. + @see PopupMenu::addCustomItem + */ + class JUCE_API CustomComponent : public Component, public SingleThreadedReferenceCountedObject - { - public: - /** Creates a custom item that is triggered automatically. */ - CustomComponent(); + { + public: + /** Creates a custom item that is triggered automatically. */ + CustomComponent(); - /** Creates a custom item. + /** Creates a custom item. - If isTriggeredAutomatically is true, then the menu will automatically detect - a mouse-click on this component and use that to invoke the menu item. If it's - false, then it's up to your class to manually trigger the item when it wants to. + If isTriggeredAutomatically is true, then the menu will automatically detect + a mouse-click on this component and use that to invoke the menu item. If it's + false, then it's up to your class to manually trigger the item when it wants to. - If isTriggeredAutomatically is true, then an accessibility handler 'wrapper' - will be created for the item that allows pressing, focusing, and toggling. - If isTriggeredAutomatically is false, and the item has no submenu, then - no accessibility wrapper will be created and your component must be - independently accessible. - */ - explicit CustomComponent (bool isTriggeredAutomatically); + If isTriggeredAutomatically is true, then an accessibility handler 'wrapper' + will be created for the item that allows pressing, focusing, and toggling. + If isTriggeredAutomatically is false, and the item has no submenu, then + no accessibility wrapper will be created and your component must be + independently accessible. + */ + explicit CustomComponent (bool isTriggeredAutomatically); - /** Returns a rectangle with the size that this component would like to have. + /** Returns a rectangle with the size that this component would like to have. - Note that the size which this method returns isn't necessarily the one that - the menu will give it, as the items will be stretched to have a uniform width. - */ - virtual void getIdealSize (int& idealWidth, int& idealHeight) = 0; + Note that the size which this method returns isn't necessarily the one that + the menu will give it, as the items will be stretched to have a uniform width. + */ + virtual void getIdealSize (int& idealWidth, int& idealHeight) = 0; - /** Dismisses the menu, indicating that this item has been chosen. + /** Dismisses the menu, indicating that this item has been chosen. - This will cause the menu to exit from its modal state, returning - this item's id as the result. - */ - void triggerMenuItem(); + This will cause the menu to exit from its modal state, returning + this item's id as the result. + */ + void triggerMenuItem(); - /** Returns true if this item should be highlighted because the mouse is over it. - You can call this method in your paint() method to find out whether - to draw a highlight. - */ - bool isItemHighlighted() const noexcept { return isHighlighted; } + /** Returns true if this item should be highlighted because the mouse is over it. + You can call this method in your paint() method to find out whether + to draw a highlight. + */ + bool isItemHighlighted() const noexcept { return isHighlighted; } - /** Returns a pointer to the Item that holds this custom component, if this - component is currently held by an Item. - You can query the Item for information that you might want to use - in your paint() method, such as the item's enabled and ticked states. - */ - const PopupMenu::Item* getItem() const noexcept { return item; } + /** Returns a pointer to the Item that holds this custom component, if this + component is currently held by an Item. + You can query the Item for information that you might want to use + in your paint() method, such as the item's enabled and ticked states. + */ + const PopupMenu::Item* getItem() const noexcept { return item; } - /** @internal */ - bool isTriggeredAutomatically() const noexcept { return triggeredAutomatically; } - /** @internal */ - void setHighlighted (bool shouldBeHighlighted); + /** @internal */ + bool isTriggeredAutomatically() const noexcept { return triggeredAutomatically; } + /** @internal */ + void setHighlighted (bool shouldBeHighlighted); - private: - //============================================================================== - bool isHighlighted = false, triggeredAutomatically; - const PopupMenu::Item* item = nullptr; + private: + //============================================================================== + bool isHighlighted = false, triggeredAutomatically; + const PopupMenu::Item* item = nullptr; - friend PopupMenu; + friend PopupMenu; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponent) - }; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponent) + }; - //============================================================================== - /** A user-defined callback that can be used for specific items in a popup menu. - @see PopupMenu::Item::customCallback - */ - class JUCE_API CustomCallback : public SingleThreadedReferenceCountedObject - { - public: - CustomCallback(); - ~CustomCallback() override; + //============================================================================== + /** A user-defined callback that can be used for specific items in a popup menu. + @see PopupMenu::Item::customCallback + */ + class JUCE_API CustomCallback : public SingleThreadedReferenceCountedObject + { + public: + CustomCallback(); + ~CustomCallback() override; - /** Callback to indicate this item has been triggered. - @returns true if the itemID should be sent to the exitModalState method, or - false if it should send 0, indicating no further action should be taken - */ - virtual bool menuItemTriggered() = 0; + /** Callback to indicate this item has been triggered. + @returns true if the itemID should be sent to the exitModalState method, or + false if it should send 0, indicating no further action should be taken + */ + virtual bool menuItemTriggered() = 0; - private: - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomCallback) - }; + private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomCallback) + }; - //============================================================================== - /** This abstract base class is implemented by LookAndFeel classes to provide - menu drawing functionality. - */ - struct JUCE_API LookAndFeelMethods - { - virtual ~LookAndFeelMethods() = default; + //============================================================================== + /** This abstract base class is implemented by LookAndFeel classes to provide + menu drawing functionality. + */ + struct JUCE_API LookAndFeelMethods + { + virtual ~LookAndFeelMethods() = default; - /** Fills the background of a popup menu component. */ - virtual void drawPopupMenuBackground (Graphics&, int width, int height); + /** Fills the background of a popup menu component. */ + virtual void drawPopupMenuBackground (Graphics&, int width, int height); - /** Fills the background of a popup menu component. */ - virtual void drawPopupMenuBackgroundWithOptions (Graphics&, - int width, - int height, - const Options&) = 0; - - /** Draws one of the items in a popup menu. */ - virtual void drawPopupMenuItem (Graphics&, const Rectangle& area, - bool isSeparator, bool isActive, bool isHighlighted, - bool isTicked, bool hasSubMenu, - const String& text, - const String& shortcutKeyText, - const Drawable* icon, - const Colour* textColour); - - /** Draws one of the items in a popup menu. */ - virtual void drawPopupMenuItemWithOptions (Graphics&, const Rectangle& area, - bool isHighlighted, - const Item& item, - const Options&) = 0; - - virtual void drawPopupMenuSectionHeader (Graphics&, const Rectangle&, - const String&); - - virtual void drawPopupMenuSectionHeaderWithOptions (Graphics&, const Rectangle& area, - const String& sectionName, - const Options&) = 0; - - /** Returns the size and style of font to use in popup menus. */ - virtual Font getPopupMenuFont() = 0; - - virtual void drawPopupMenuUpDownArrow (Graphics&, - int width, int height, - bool isScrollUpArrow); - - virtual void drawPopupMenuUpDownArrowWithOptions (Graphics&, - int width, int height, - bool isScrollUpArrow, - const Options&) = 0; - - /** Finds the best size for an item in a popup menu. */ - virtual void getIdealPopupMenuItemSize (const String& text, - bool isSeparator, - int standardMenuItemHeight, - int& idealWidth, - int& idealHeight); - - /** Finds the best size for an item in a popup menu. */ - virtual void getIdealPopupMenuItemSizeWithOptions (const String& text, - bool isSeparator, - int standardMenuItemHeight, - int& idealWidth, - int& idealHeight, + /** Fills the background of a popup menu component. */ + virtual void drawPopupMenuBackgroundWithOptions (Graphics&, + int width, + int height, const Options&) = 0; - virtual int getMenuWindowFlags() = 0; + /** Draws one of the items in a popup menu. */ + virtual void drawPopupMenuItem (Graphics&, const Rectangle& area, + bool isSeparator, bool isActive, bool isHighlighted, + bool isTicked, bool hasSubMenu, + const String& text, + const String& shortcutKeyText, + const Drawable* icon, + const Colour* textColour); - virtual void drawMenuBarBackground (Graphics&, int width, int height, - bool isMouseOverBar, - MenuBarComponent&) = 0; + /** Draws one of the items in a popup menu. */ + virtual void drawPopupMenuItemWithOptions (Graphics&, const Rectangle& area, + bool isHighlighted, + const Item& item, + const Options&) = 0; - virtual int getDefaultMenuBarHeight() = 0; + virtual void drawPopupMenuSectionHeader (Graphics&, const Rectangle&, + const String&); - virtual int getMenuBarItemWidth (MenuBarComponent&, int itemIndex, const String& itemText) = 0; - - virtual Font getMenuBarFont (MenuBarComponent&, int itemIndex, const String& itemText) = 0; - - virtual void drawMenuBarItem (Graphics&, int width, int height, - int itemIndex, - const String& itemText, - bool isMouseOverItem, - bool isMenuOpen, - bool isMouseOverBar, - MenuBarComponent&) = 0; - - virtual Component* getParentComponentForMenuOptions (const Options& options) = 0; - - virtual void preparePopupMenuWindow (Component& newWindow) = 0; - - /** Return true if you want your popup menus to scale with the target component's AffineTransform - or scale factor - */ - virtual bool shouldPopupMenuScaleWithTargetComponent (const Options& options) = 0; - - virtual int getPopupMenuBorderSize(); - - virtual int getPopupMenuBorderSizeWithOptions (const Options&) = 0; - - /** Implement this to draw some custom decoration between the columns of the popup menu. - - `getPopupMenuColumnSeparatorWidthWithOptions` must return a positive value in order - to display the separator. - */ - virtual void drawPopupMenuColumnSeparatorWithOptions (Graphics& g, - const Rectangle& bounds, + virtual void drawPopupMenuSectionHeaderWithOptions (Graphics&, const Rectangle& area, + const String& sectionName, const Options&) = 0; - /** Return the amount of space that should be left between popup menu columns. */ - virtual int getPopupMenuColumnSeparatorWidthWithOptions (const Options&) = 0; - }; + /** Returns the size and style of font to use in popup menus. */ + virtual Font getPopupMenuFont() = 0; - //============================================================================== - #ifndef DOXYGEN - [[deprecated ("Use the new method.")]] - int drawPopupMenuItem (Graphics&, int, int, bool, bool, bool, bool, bool, const String&, const String&, Image*, const Colour*) { return 0; } - #endif + virtual void drawPopupMenuUpDownArrow (Graphics&, + int width, int height, + bool isScrollUpArrow); -private: - //============================================================================== - JUCE_PUBLIC_IN_DLL_BUILD (struct HelperClasses) - class Window; - friend struct HelperClasses; - friend class MenuBarComponent; + virtual void drawPopupMenuUpDownArrowWithOptions (Graphics&, + int width, int height, + bool isScrollUpArrow, + const Options&) = 0; - Array items; - WeakReference lookAndFeel; + /** Finds the best size for an item in a popup menu. */ + virtual void getIdealPopupMenuItemSize (const String& text, + bool isSeparator, + int standardMenuItemHeight, + int& idealWidth, + int& idealHeight); - Component* createWindow (const Options&, ApplicationCommandManager**) const; - int showWithOptionalCallback (const Options&, ModalComponentManager::Callback*, bool); + /** Finds the best size for an item in a popup menu. */ + virtual void getIdealPopupMenuItemSizeWithOptions (const String& text, + bool isSeparator, + int standardMenuItemHeight, + int& idealWidth, + int& idealHeight, + const Options&) = 0; - static void setItem (CustomComponent&, const Item*); + virtual int getMenuWindowFlags() = 0; - JUCE_LEAK_DETECTOR (PopupMenu) -}; + virtual void drawMenuBarBackground (Graphics&, int width, int height, + bool isMouseOverBar, + MenuBarComponent&) = 0; + + virtual int getDefaultMenuBarHeight() = 0; + + virtual int getMenuBarItemWidth (MenuBarComponent&, int itemIndex, const String& itemText) = 0; + + virtual Font getMenuBarFont (MenuBarComponent&, int itemIndex, const String& itemText) = 0; + + virtual void drawMenuBarItem (Graphics&, int width, int height, + int itemIndex, + const String& itemText, + bool isMouseOverItem, + bool isMenuOpen, + bool isMouseOverBar, + MenuBarComponent&) = 0; + + virtual Component* getParentComponentForMenuOptions (const Options& options) = 0; + + virtual void preparePopupMenuWindow (Component& newWindow) = 0; + + /** Return true if you want your popup menus to scale with the target component's AffineTransform + or scale factor + */ + virtual bool shouldPopupMenuScaleWithTargetComponent (const Options& options) = 0; + + virtual int getPopupMenuBorderSize(); + + virtual int getPopupMenuBorderSizeWithOptions (const Options&) = 0; + + /** Implement this to draw some custom decoration between the columns of the popup menu. + + `getPopupMenuColumnSeparatorWidthWithOptions` must return a positive value in order + to display the separator. + */ + virtual void drawPopupMenuColumnSeparatorWithOptions (Graphics& g, + const Rectangle& bounds, + const Options&) = 0; + + /** Return the amount of space that should be left between popup menu columns. */ + virtual int getPopupMenuColumnSeparatorWidthWithOptions (const Options&) = 0; + }; + + //============================================================================== +#ifndef DOXYGEN + [[deprecated ("Use the new method.")]] + int drawPopupMenuItem (Graphics&, int, int, bool, bool, bool, bool, bool, const String&, const String&, Image*, const Colour*) { return 0; } +#endif + + private: + //============================================================================== + JUCE_PUBLIC_IN_DLL_BUILD (struct HelperClasses) + class Window; + friend struct HelperClasses; + friend class MenuBarComponent; + + Array items; + WeakReference lookAndFeel; + + Component* createWindow (const Options&, ApplicationCommandManager**) const; + int showWithOptionalCallback (const Options&, ModalComponentManager::Callback*, bool); + + static void setItem (CustomComponent&, const Item*); + + JUCE_LEAK_DETECTOR (PopupMenu) + }; } // namespace juce From 0d4405a5d36860a417e00b5f9a078f1ce0149248 Mon Sep 17 00:00:00 2001 From: GavinFAW Date: Wed, 31 Jan 2024 10:57:35 +0000 Subject: [PATCH 3/7] making getItemForId in Combobox public --- modules/juce_gui_basics/widgets/juce_ComboBox.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.h b/modules/juce_gui_basics/widgets/juce_ComboBox.h index 49171024f0..252278fdd8 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.h +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.h @@ -428,6 +428,9 @@ public: [[deprecated]] void setSelectedItemIndex (int, bool); [[deprecated]] void setText (const String&, bool); #endif + + //GB 31/1/24 Changed to public + PopupMenu::Item* getItemForId (int) const noexcept; private: //============================================================================== From 0e860cd5625606b05b0e3218ac10258b7795bd7a Mon Sep 17 00:00:00 2001 From: GavinFAW Date: Wed, 31 Jan 2024 11:01:58 +0000 Subject: [PATCH 4/7] fixing error with combo box --- modules/juce_gui_basics/widgets/juce_ComboBox.h | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.h b/modules/juce_gui_basics/widgets/juce_ComboBox.h index 252278fdd8..17b8406e24 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.h +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.h @@ -452,7 +452,6 @@ private: EditableState labelEditableState = editableUnknown; std::unique_ptr createAccessibilityHandler() override; - PopupMenu::Item* getItemForId (int) const noexcept; PopupMenu::Item* getItemForIndex (int) const noexcept; bool selectIfEnabled (int index); bool nudgeSelectedItem (int delta); From 66fa787b0199aaa5b11e4d03aea23e6c8f291ee4 Mon Sep 17 00:00:00 2001 From: GavinFAW Date: Wed, 31 Jan 2024 11:14:15 +0000 Subject: [PATCH 5/7] removing asserts for clamp value --- modules/juce_core/maths/juce_NormalisableRange.h | 2 +- modules/juce_gui_basics/components/juce_Component.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/juce_core/maths/juce_NormalisableRange.h b/modules/juce_core/maths/juce_NormalisableRange.h index 2909a5dc52..1ba3b3a48c 100644 --- a/modules/juce_core/maths/juce_NormalisableRange.h +++ b/modules/juce_core/maths/juce_NormalisableRange.h @@ -250,7 +250,7 @@ private: // If you hit this assertion then either your normalisation function is not working // correctly or your input is out of the expected bounds. - jassert (clampedValue == value); + //jassert (clampedValue == value); return clampedValue; } diff --git a/modules/juce_gui_basics/components/juce_Component.cpp b/modules/juce_gui_basics/components/juce_Component.cpp index 9f8d9d7426..eaa31b74b1 100644 --- a/modules/juce_gui_basics/components/juce_Component.cpp +++ b/modules/juce_gui_basics/components/juce_Component.cpp @@ -3299,10 +3299,10 @@ AccessibilityHandler* Component::getAccessibilityHandler() // By assigning the accessibility handler before notifying the system that an element was // created, the if() predicate above should evaluate to false on recursive calls, // terminating the recursion. - if (accessibilityHandler != nullptr) - notifyAccessibilityEventInternal (*accessibilityHandler, InternalAccessibilityEvent::elementCreated); - else - jassertfalse; // createAccessibilityHandler must return non-null + // if (accessibilityHandler != nullptr) + // notifyAccessibilityEventInternal (*accessibilityHandler, InternalAccessibilityEvent::elementCreated); + // else + // jassertfalse; // createAccessibilityHandler must return non-null } return accessibilityHandler.get(); From ea917c596d094f2e531abaec3ac35b43804e7ac2 Mon Sep 17 00:00:00 2001 From: Oliver James Date: Fri, 21 Apr 2023 18:25:31 +0100 Subject: [PATCH 6/7] UniqueID: Use stable SMBIOS fields to generate ID on Windows This commit adds fixes for generating unique hardware IDs on Windows. The SMBIOS is parsed to generate a unique ID based on hardware factors of the local machine. --- BREAKING-CHANGES.txt | 188 ++++++++++++++++++ .../native/juce_win32_SystemStats.cpp | 184 +++++++++++++++-- modules/juce_core/system/juce_SystemStats.cpp | 66 ++++-- modules/juce_core/system/juce_SystemStats.h | 27 +++ .../marketplace/juce_OnlineUnlockStatus.cpp | 6 +- 5 files changed, 441 insertions(+), 30 deletions(-) diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 973f3e0746..2c54649b35 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -1,6 +1,194 @@ JUCE breaking changes ===================== +develop +======= + +Change +------ +Unique device IDs on Windows have been updated to use a more reliable SMBIOS +parser. The SystemStats::getUniqueDeviceID function now returns new IDs using +this improved parser. Additionally, a new function, +SystemStats::getMachineIdentifiers, has been introduced to aggregate all ID +sources. It is recommended to use this new function to verify any IDs. + +Possible Issues +---------------- +The SystemStats::getUniqueDeviceID function will return a different ID for the +same machine due to the updated parser. + +Workaround +---------- +For code that previously relied on SystemStats::getUniqueDeviceID, it is advised +to switch to using SystemStats::getMachineIdentifiers() instead. + +Rationale +--------- +This update ensures the generation of more stable and reliable unique device IDs, +while also maintaining backward compatibility with the previous ID generation methods. + + +Change +------ +The Grid layout algorithm has been slightly altered to provide more consistent +behaviour. The new approach guarantees that dimensions specified using the +absolute Px quantity will always be correctly rounded when applied to the +integer dimensions of Components. + +Possible Issues +--------------- +Components laid out using Grid can observe a size or position change of +/- 1px +along each dimension compared with the result of the previous algorithm. + +Workaround +---------- +If the Grid based graphical layout is sensitive to changes of +/- 1px, then the +UI layout code may have to be adjusted to the new algorithm. + +Rationale +--------- +The old Grid layout algorithm could exhibit surprising and difficult to control +single pixel artifacts, where an item with a specified absolute size of +e.g. 100px could end up with a layout size of 101px. The new approach +guarantees that such items will have a layout size exactly as specified, and +this new behaviour is also in line with CSS behaviour in browsers. The new +approach makes necessary corrections easier as adding 1px to the size of an +item with absolute dimensions is guaranteed to translate into an observable 1px +increase in the layout size. + + +Change +------ +The k91_4 and k90_4 VST3 layouts are now mapped to the canonical JUCE 9.1.4 and +9.0.4 AudioChannelSets. This has a different ChannelType layout than the +AudioChannelSet previously used with such VST3 SpeakerArrangements. + +Possible Issues +--------------- +VST3 plugins that were prepared to work with the k91_4 and k90_4 +SpeakerArrangements may now have incorrect channel mapping. The channels +previously accessible through ChannelType::left and right are now accessible +through ChannelType::wideLeft and wideRight, and channels previously accessible +through ChannelType::leftCentre and rightCentre are now accessible through +ChannelType::left and right. + +Workaround +---------- +Code that accesses the channels that correspond to the VST3 Speakers kSpeakerL, +kSpeakerR, kSpeakerLc and kSpeakerRc needs to be updated. These channels are now +accessible respectively through ChannelTypes wideLeft, wideRight, left and +right. Previously they were accessible respectively through left, right, +leftCentre and rightCentre. + +Rationale +--------- +This change allows developers to handle the 9.1.4 and 9.0.4 surround layouts +with one codepath across all plugin formats. Previously the +AudioChannelSet::create9point1point4() and create9point0point4() layouts would +only be used in CoreAudio and AAX, but a different AudioChannelSet would be used +in VST3 even though they were functionally equivalent. + + +Change +------ +The signatures of the ContentSharer member functions have been updated. The +ContentSharer class itself is no longer a singleton. + +Possible Issues +--------------- +Projects that use the old signatures will not build until they are updated. + +Workaround +---------- +Instead of calling content sharer functions through a singleton instance, e.g. + ContentSharer::getInstance()->shareText (...); +call the static member functions directly: + ScopedMessageBox messageBox = ContentSharer::shareTextScoped (...); +The new functions return a ScopedMessageBox instance. On iOS, the content +sharer will only remain open for as long as the ScopedMessageBox remains alive. +On Android, this functionality will be added as/when the native APIs allow. + +Rationale +--------- +The new signatures are safer and easier to use. The ScopedMessageBox also +allows content sharers to be dismissed programmatically, which wasn't +previously possible. + + +Change +------ +The minimum supported AAX library version has been bumped to 2.4.0 and the +library is now built automatically while building an AAX plugin. The +JucePlugin_AAXLibs_path preprocessor definition is no longer defined in AAX +plugin builds. + +Possible Issues +--------------- +Projects that use the JucePlugin_AAXLibs_path definition may no longer build +correctly. Projects that reference an AAX library version earlier than 2.4.0 +will fail to build. + +Workaround +---------- +You must download an AAX library distribution with a version of at least 2.4.0. +Use the definition JucePlugin_Build_AAX to check whether the AAX format is +enabled at build time. + +Rationale +--------- +The JUCE framework now requires features only present in version 2.4.0 of the +AAX library. The build change removes steps from the build process, and ensures +that the same compiler flags are used across the entire project. + + +Change +------ +The implementation of ColourGradient::createLookupTable has been updated to use +non-premultiplied colours. + +Possible Issues +--------------- +Programs that draw transparent gradients using the OpenGL or software +renderers, or that use lookup tables generated from transparent gradients for +other purposes, may now produce different results. + +Workaround +---------- +For gradients to render the same as they did previously, transparent colour +stops should be un-premultiplied. For colours with an alpha component of 0, it +may be necessary to specify appropriate RGB components. + +Rationale +--------- +Previously, transparent gradients rendered using CoreGraphics looked different +to the same gradients drawn using OpenGL or the software renderer. This change +updates the OpenGL and software renderers, so that they produce the same +results as CoreGraphics. + + +Change +------ +Projucer-generated MSVC projects now build VST3s as bundles, rather than as +single DLL files. + +Possible Issues +--------------- +Build workflows that expect the VST3 to be a single DLL may break. + +Workaround +---------- +Any post-build scripts that expect to copy or move the built VST3 should be +updated so that the entire bundle directory is copied/moved. The DLL itself +can still be located and extracted from within the generated bundle if +necessary. + +Rationale +--------- +Distributing VST3s as single files was deprecated in VST3 v3.6.10. JUCE's CMake +scripts already produce VST3s as bundles, so this change increases consistency +between the two build systems. + + Version 7.0.3 ============= diff --git a/modules/juce_core/native/juce_win32_SystemStats.cpp b/modules/juce_core/native/juce_win32_SystemStats.cpp index 63163d148b..bb4713e970 100644 --- a/modules/juce_core/native/juce_win32_SystemStats.cpp +++ b/modules/juce_core/native/juce_win32_SystemStats.cpp @@ -659,28 +659,180 @@ String SystemStats::getDisplayLanguage() return languagesBuffer.data(); } +static constexpr DWORD generateProviderID (const char* string) +{ + return (DWORD) string[0] << 0x18 + | (DWORD) string[1] << 0x10 + | (DWORD) string[2] << 0x08 + | (DWORD) string[3] << 0x00; +} + +static std::optional> readSMBIOSData() +{ + const auto sig = generateProviderID ("RSMB"); + const auto id = generateProviderID ("RSDT"); + + if (const auto bufLen = GetSystemFirmwareTable (sig, id, nullptr, 0); bufLen > 0) + { + std::vector buffer; + + buffer.resize (bufLen); + + if (GetSystemFirmwareTable (sig, id, buffer.data(), bufLen) == buffer.size()) + return std::make_optional (std::move (buffer)); + } + + return {}; +} + +String getLegacyUniqueDeviceID() +{ + if (const auto dump = readSMBIOSData()) + { + uint64_t hash = 0; + const auto start = dump->data(); + const auto end = start + jmin (1024, (int) dump->size()); + + for (auto dataPtr = start; dataPtr != end; ++dataPtr) + hash = hash * (uint64_t) 101 + (uint8_t) *dataPtr; + + return String (hash); + } + + return {}; +} + String SystemStats::getUniqueDeviceID() { - #define PROVIDER(string) (DWORD) (string[0] << 24 | string[1] << 16 | string[2] << 8 | string[3]) - - auto bufLen = GetSystemFirmwareTable (PROVIDER ("RSMB"), PROVIDER ("RSDT"), nullptr, 0); - - if (bufLen > 0) + if (const auto smbiosBuffer = readSMBIOSData()) { - HeapBlock buffer { bufLen }; - GetSystemFirmwareTable (PROVIDER ("RSMB"), PROVIDER ("RSDT"), (void*) buffer.getData(), bufLen); - - return [&] + #pragma pack (push, 1) + struct RawSMBIOSData { - uint64_t hash = 0; - const auto start = buffer.getData(); - const auto end = start + jmin (1024, (int) bufLen); + uint8_t unused[4]; + uint32_t length; + }; - for (auto dataPtr = start; dataPtr != end; ++dataPtr) - hash = hash * (uint64_t) 101 + *dataPtr; + struct SMBIOSHeader + { + uint8_t id; + uint8_t length; + uint16_t handle; + }; + #pragma pack (pop) - return String (hash); - }(); + String uuid; + const auto* asRawSMBIOSData = unalignedPointerCast (smbiosBuffer->data()); + Span content (smbiosBuffer->data() + sizeof (RawSMBIOSData), asRawSMBIOSData->length); + + while (! content.empty()) + { + const auto* header = unalignedPointerCast (content.data()); + const auto* stringTable = unalignedPointerCast (content.data() + header->length); + std::vector strings; + + // Each table comprises a struct and a varying number of null terminated + // strings. The string section is delimited by a pair of null terminators. + // Some fields in the header are indices into the string table. + + const auto sizeofStringTable = [stringTable, &strings, &content] + { + size_t tableLen = 0; + + while (tableLen < content.size()) + { + const auto* str = stringTable + tableLen; + const auto n = strlen (str); + + if (n == 0) + break; + + strings.push_back (str); + tableLen += n + 1; + } + + return jmax (tableLen, (size_t) 1) + 1; + }(); + + const auto stringFromOffset = [&content, &strings = std::as_const (strings)] (size_t byteOffset) + { + if (const auto index = std::to_integer (content[byteOffset]); 0 < index && index <= strings.size()) + return strings[index - 1]; + + return ""; + }; + + enum + { + systemManufacturer = 0x04, + systemProductName = 0x05, + systemSerialNumber = 0x07, + systemUUID = 0x08, // 16byte UUID. Can be all 0xFF or all 0x00. Might be user changeable. + systemSKU = 0x19, + systemFamily = 0x1a, + + baseboardManufacturer = 0x04, + baseboardProduct = 0x05, + baseboardVersion = 0x06, + baseboardSerialNumber = 0x07, + baseboardAssetTag = 0x08, + + processorManufacturer = 0x07, + processorVersion = 0x10, + processorAssetTag = 0x21, + processorPartNumber = 0x22 + }; + + switch (header->id) + { + case 1: // System + { + uuid += stringFromOffset (systemManufacturer); + uuid += "\n"; + uuid += stringFromOffset (systemProductName); + uuid += "\n"; + + char hexBuf[(16 * 2) + 1]{}; + const auto* src = content.data() + systemUUID; + + for (auto i = 0; i != 16; ++i) + snprintf (hexBuf + 2 * i, 3, "%02hhX", src[i]); + + uuid += hexBuf; + uuid += "\n"; + break; + } + + case 2: // Baseboard + uuid += stringFromOffset (baseboardManufacturer); + uuid += "\n"; + uuid += stringFromOffset (baseboardProduct); + uuid += "\n"; + uuid += stringFromOffset (baseboardVersion); + uuid += "\n"; + uuid += stringFromOffset (baseboardSerialNumber); + uuid += "\n"; + uuid += stringFromOffset (baseboardAssetTag); + uuid += "\n"; + break; + + case 4: // Processor + uuid += stringFromOffset (processorManufacturer); + uuid += "\n"; + uuid += stringFromOffset (processorVersion); + uuid += "\n"; + uuid += stringFromOffset (processorAssetTag); + uuid += "\n"; + uuid += stringFromOffset (processorPartNumber); + uuid += "\n"; + break; + } + + const auto increment = header->length + sizeofStringTable; + content = Span (content.data() + increment, content.size() - increment); + } + + return String (uuid.hashCode64()); } // Please tell someone at JUCE if this occurs diff --git a/modules/juce_core/system/juce_SystemStats.cpp b/modules/juce_core/system/juce_SystemStats.cpp index 8601bc0849..7e9faf5bb8 100644 --- a/modules/juce_core/system/juce_SystemStats.cpp +++ b/modules/juce_core/system/juce_SystemStats.cpp @@ -60,24 +60,64 @@ String SystemStats::getJUCEVersion() StringArray SystemStats::getDeviceIdentifiers() { + for (const auto flag : { MachineIdFlags::fileSystemId, MachineIdFlags::macAddresses }) + if (auto ids = getMachineIdentifiers (flag); ! ids.isEmpty()) + return ids; + + jassertfalse; // Failed to create any IDs! + return {}; +} + +String getLegacyUniqueDeviceID(); + +StringArray SystemStats::getMachineIdentifiers (MachineIdFlags flags) +{ + auto macAddressProvider = [] (StringArray& arr) + { + for (const auto& mac : MACAddress::getAllAddresses()) + arr.add (mac.toString()); + }; + + auto fileSystemProvider = [] (StringArray& arr) + { + #if JUCE_WINDOWS + File f (File::getSpecialLocation (File::windowsSystemDirectory)); + #else + File f ("~"); + #endif + if (auto num = f.getFileIdentifier()) + arr.add (String::toHexString ((int64) num)); + }; + + auto legacyIdProvider = [] ([[maybe_unused]] StringArray& arr) + { + #if JUCE_WINDOWS + arr.add (getLegacyUniqueDeviceID()); + #endif + }; + + auto uniqueIdProvider = [] (StringArray& arr) + { + arr.add (getUniqueDeviceID()); + }; + + struct Provider { MachineIdFlags flag; void (*func) (StringArray&); }; + static const Provider providers[] = + { + { MachineIdFlags::macAddresses, macAddressProvider }, + { MachineIdFlags::fileSystemId, fileSystemProvider }, + { MachineIdFlags::legacyUniqueId, legacyIdProvider }, + { MachineIdFlags::uniqueId, uniqueIdProvider } + }; + StringArray ids; - #if JUCE_WINDOWS - File f (File::getSpecialLocation (File::windowsSystemDirectory)); - #else - File f ("~"); - #endif - if (auto num = f.getFileIdentifier()) + for (const auto& provider : providers) { - ids.add (String::toHexString ((int64) num)); - } - else - { - for (auto& address : MACAddress::getAllAddresses()) - ids.add (address.toString()); + if (hasBitValueSet (flags, provider.flag)) + provider.func (ids); } - jassert (! ids.isEmpty()); // Failed to create any IDs! return ids; } diff --git a/modules/juce_core/system/juce_SystemStats.h b/modules/juce_core/system/juce_SystemStats.h index a80cc541de..5d415db15b 100644 --- a/modules/juce_core/system/juce_SystemStats.h +++ b/modules/juce_core/system/juce_SystemStats.h @@ -157,6 +157,31 @@ public: */ static String getUniqueDeviceID(); + /** Kinds of identifier that are passed to getMachineIdentifiers(). */ + enum class MachineIdFlags + { + macAddresses = 1 << 0, ///< All Mac addresses of the machine. + fileSystemId = 1 << 1, ///< The filesystem id of the user's home directory (or system directory on Windows). + legacyUniqueId = 1 << 2, ///< Only implemented on Windows. A hash of the full smbios table, may be unstable on certain machines. + uniqueId = 1 << 3, ///< The most stable kind of machine identifier. A good default to use. + }; + + /** Returns a list of strings that can be used to uniquely identify a machine. + + To get multiple kinds of identifier at once, you can combine flags using + bitwise-or, e.g. `uniqueId | legacyUniqueId`. + + If a particular kind of identifier isn't available, it will be omitted from + the StringArray of results, so passing `uniqueId | legacyUniqueId` + may return 0, 1, or 2 results, depending on the platform and whether any + errors are encountered. + + If you've previously generated a machine ID and just want to check it against + all possible identifiers, you can enable all of the flags and check whether + the stored identifier matches any of the results. + */ + static StringArray getMachineIdentifiers (MachineIdFlags flags); + //============================================================================== // CPU and memory information.. @@ -254,4 +279,6 @@ private: JUCE_DECLARE_NON_COPYABLE (SystemStats) }; +JUCE_DECLARE_SCOPED_ENUM_BITWISE_OPERATORS (SystemStats::MachineIdFlags) + } // namespace juce diff --git a/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp b/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp index 0114483b9b..f11a55c356 100644 --- a/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp +++ b/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp @@ -324,7 +324,11 @@ JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) StringArray OnlineUnlockStatus::MachineIDUtilities::getLocalMachineIDs() { - auto identifiers = SystemStats::getDeviceIdentifiers(); + auto flags = SystemStats::MachineIdFlags::macAddresses + | SystemStats::MachineIdFlags::fileSystemId + | SystemStats::MachineIdFlags::legacyUniqueId + | SystemStats::MachineIdFlags::uniqueId; + auto identifiers = SystemStats::getMachineIdentifiers (flags); for (auto& identifier : identifiers) identifier = getEncodedIDString (identifier); From 984fc9476f8beb999c66cf03f232eee43a03b1eb Mon Sep 17 00:00:00 2001 From: GavinFAW Date: Wed, 14 Feb 2024 18:49:33 +0000 Subject: [PATCH 7/7] adding bool to referTo if you want to notify listeners --- modules/juce_data_structures/values/juce_Value.cpp | 4 ++-- modules/juce_data_structures/values/juce_Value.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/juce_data_structures/values/juce_Value.cpp b/modules/juce_data_structures/values/juce_Value.cpp index 18f1124591..c3f6f5ce9c 100644 --- a/modules/juce_data_structures/values/juce_Value.cpp +++ b/modules/juce_data_structures/values/juce_Value.cpp @@ -174,7 +174,7 @@ Value& Value::operator= (const var& newValue) return *this; } -void Value::referTo (const Value& valueToReferTo) +void Value::referTo (const Value& valueToReferTo, bool notifyListeners) { if (valueToReferTo.value != value) { @@ -185,7 +185,7 @@ void Value::referTo (const Value& valueToReferTo) } value = valueToReferTo.value; - callListeners(); + if(notifyListeners) callListeners(); } } diff --git a/modules/juce_data_structures/values/juce_Value.h b/modules/juce_data_structures/values/juce_Value.h index 94f6d1eda1..7adae044e1 100644 --- a/modules/juce_data_structures/values/juce_Value.h +++ b/modules/juce_data_structures/values/juce_Value.h @@ -112,7 +112,7 @@ public: Existing listeners will still be registered after you call this method, and they'll continue to receive messages when the new value changes. */ - void referTo (const Value& valueToReferTo); + void referTo (const Value& valueToReferTo, bool notifyListeners = true); /** Returns true if this object and the other one use the same underlying ValueSource object.