mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-20 01:14:20 +00:00
Ideally, we want to pass shortcut keys to the component to handle, and only fall back to invoking a menu item if the component was unable to handle the keyboard event, or if the action was triggered by clicking/selecting an item in the menu itself. The old implementation tried to work out whether the action was triggered by a shortcut by checking the event's characters and modifiers. This method was inaccurate, because some shortcuts (such as arrow keys) may add unexpected numpad/function modifier flags. We now try handling shortcut keys directly in the peer, and pass events up to the superclass (which will forward them to the main menu) if the event could not be handled. This commit also adjusts some Objective-C method signatures to use the correct string encoding for the BOOL type.
813 lines
29 KiB
Text
813 lines
29 KiB
Text
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2020 - Raw Material Software Limited
|
|
|
|
JUCE is an open source library subject to commercial or open-source
|
|
licensing.
|
|
|
|
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
|
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
|
|
|
End User License Agreement: www.juce.com/juce-6-licence
|
|
Privacy Policy: www.juce.com/juce-privacy-policy
|
|
|
|
Or: You may also use this code under the terms of the GPL v3 (see
|
|
www.gnu.org/licenses).
|
|
|
|
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
|
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
|
DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace juce
|
|
{
|
|
|
|
//==============================================================================
|
|
struct JuceMainMenuBarHolder : private DeletedAtShutdown
|
|
{
|
|
JuceMainMenuBarHolder()
|
|
: mainMenuBar ([[NSMenu alloc] initWithTitle: nsStringLiteral ("MainMenu")])
|
|
{
|
|
auto item = [mainMenuBar addItemWithTitle: nsStringLiteral ("Apple")
|
|
action: nil
|
|
keyEquivalent: nsEmptyString()];
|
|
|
|
auto appMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Apple")];
|
|
|
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
|
[NSApp performSelector: @selector (setAppleMenu:) withObject: appMenu];
|
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
|
|
|
[mainMenuBar setSubmenu: appMenu forItem: item];
|
|
[appMenu release];
|
|
|
|
[NSApp setMainMenu: mainMenuBar];
|
|
}
|
|
|
|
~JuceMainMenuBarHolder()
|
|
{
|
|
clearSingletonInstance();
|
|
|
|
[NSApp setMainMenu: nil];
|
|
[mainMenuBar release];
|
|
}
|
|
|
|
NSMenu* mainMenuBar = nil;
|
|
|
|
JUCE_DECLARE_SINGLETON_SINGLETHREADED (JuceMainMenuBarHolder, true)
|
|
};
|
|
|
|
JUCE_IMPLEMENT_SINGLETON (JuceMainMenuBarHolder)
|
|
|
|
//==============================================================================
|
|
class JuceMainMenuHandler : private MenuBarModel::Listener,
|
|
private DeletedAtShutdown
|
|
{
|
|
public:
|
|
JuceMainMenuHandler()
|
|
{
|
|
static JuceMenuCallbackClass cls;
|
|
callback = [cls.createInstance() init];
|
|
JuceMenuCallbackClass::setOwner (callback, this);
|
|
}
|
|
|
|
~JuceMainMenuHandler() override
|
|
{
|
|
setMenu (nullptr, nullptr, String());
|
|
|
|
jassert (instance == this);
|
|
instance = nullptr;
|
|
|
|
[callback release];
|
|
}
|
|
|
|
void setMenu (MenuBarModel* const newMenuBarModel,
|
|
const PopupMenu* newExtraAppleMenuItems,
|
|
const String& recentItemsName)
|
|
{
|
|
recentItemsMenuName = recentItemsName;
|
|
|
|
if (currentModel != newMenuBarModel)
|
|
{
|
|
if (currentModel != nullptr)
|
|
currentModel->removeListener (this);
|
|
|
|
currentModel = newMenuBarModel;
|
|
|
|
if (currentModel != nullptr)
|
|
currentModel->addListener (this);
|
|
|
|
menuBarItemsChanged (nullptr);
|
|
}
|
|
|
|
extraAppleMenuItems.reset (createCopyIfNotNull (newExtraAppleMenuItems));
|
|
}
|
|
|
|
void addTopLevelMenu (NSMenu* parent, const PopupMenu& child, const String& name, int menuId, int topLevelIndex)
|
|
{
|
|
NSMenuItem* item = [parent addItemWithTitle: juceStringToNS (name)
|
|
action: nil
|
|
keyEquivalent: nsEmptyString()];
|
|
|
|
NSMenu* sub = createMenu (child, name, menuId, topLevelIndex, true);
|
|
|
|
[parent setSubmenu: sub forItem: item];
|
|
[sub setAutoenablesItems: false];
|
|
[sub release];
|
|
}
|
|
|
|
void updateTopLevelMenu (NSMenuItem* parentItem, const PopupMenu& menuToCopy, const String& name, int menuId, int topLevelIndex)
|
|
{
|
|
// Note: This method used to update the contents of the existing menu in-place, but that caused
|
|
// weird side-effects which messed-up keyboard focus when switching between windows. By creating
|
|
// a new menu and replacing the old one with it, that problem seems to be avoided..
|
|
NSMenu* menu = [[NSMenu alloc] initWithTitle: juceStringToNS (name)];
|
|
|
|
for (PopupMenu::MenuItemIterator iter (menuToCopy); iter.next();)
|
|
addMenuItem (iter, menu, menuId, topLevelIndex);
|
|
|
|
[menu setAutoenablesItems: false];
|
|
[menu update];
|
|
|
|
removeItemRecursive ([parentItem submenu]);
|
|
[parentItem setSubmenu: menu];
|
|
|
|
[menu release];
|
|
}
|
|
|
|
void updateTopLevelMenu (NSMenu* menu)
|
|
{
|
|
NSMenu* superMenu = [menu supermenu];
|
|
auto menuNames = currentModel->getMenuBarNames();
|
|
auto indexOfMenu = (int) [superMenu indexOfItemWithSubmenu: menu] - 1;
|
|
|
|
if (indexOfMenu >= 0)
|
|
{
|
|
removeItemRecursive (menu);
|
|
|
|
auto updatedPopup = currentModel->getMenuForIndex (indexOfMenu, menuNames[indexOfMenu]);
|
|
|
|
for (PopupMenu::MenuItemIterator iter (updatedPopup); iter.next();)
|
|
addMenuItem (iter, menu, 1, indexOfMenu);
|
|
|
|
[menu update];
|
|
}
|
|
}
|
|
|
|
void menuBarItemsChanged (MenuBarModel*) override
|
|
{
|
|
if (isOpen)
|
|
{
|
|
defferedUpdateRequested = true;
|
|
return;
|
|
}
|
|
|
|
lastUpdateTime = Time::getMillisecondCounter();
|
|
|
|
StringArray menuNames;
|
|
if (currentModel != nullptr)
|
|
menuNames = currentModel->getMenuBarNames();
|
|
|
|
auto* menuBar = getMainMenuBar();
|
|
|
|
while ([menuBar numberOfItems] > 1 + menuNames.size())
|
|
removeItemRecursive (menuBar, static_cast<int> ([menuBar numberOfItems] - 1));
|
|
|
|
int menuId = 1;
|
|
|
|
for (int i = 0; i < menuNames.size(); ++i)
|
|
{
|
|
const PopupMenu menu (currentModel->getMenuForIndex (i, menuNames[i]));
|
|
|
|
if (i >= [menuBar numberOfItems] - 1)
|
|
addTopLevelMenu (menuBar, menu, menuNames[i], menuId, i);
|
|
else
|
|
updateTopLevelMenu ([menuBar itemAtIndex: 1 + i], menu, menuNames[i], menuId, i);
|
|
}
|
|
}
|
|
|
|
void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info) override
|
|
{
|
|
if ((info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) == 0
|
|
&& info.invocationMethod != ApplicationCommandTarget::InvocationInfo::fromKeyPress)
|
|
if (auto* item = findMenuItemWithCommandID (getMainMenuBar(), info.commandID))
|
|
flashMenuBar ([item menu]);
|
|
}
|
|
|
|
void invoke (const PopupMenu::Item& item, int topLevelIndex) const
|
|
{
|
|
if (currentModel != nullptr)
|
|
{
|
|
if (item.action != nullptr)
|
|
{
|
|
MessageManager::callAsync (item.action);
|
|
return;
|
|
}
|
|
|
|
if (item.customCallback != nullptr)
|
|
if (! item.customCallback->menuItemTriggered())
|
|
return;
|
|
|
|
if (item.commandManager != nullptr)
|
|
{
|
|
ApplicationCommandTarget::InvocationInfo info (item.itemID);
|
|
info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
|
|
|
|
item.commandManager->invoke (info, true);
|
|
}
|
|
|
|
MessageManager::callAsync ([=]
|
|
{
|
|
if (instance != nullptr)
|
|
instance->invokeDirectly (item.itemID, topLevelIndex);
|
|
});
|
|
}
|
|
}
|
|
|
|
void invokeDirectly (int commandId, int topLevelIndex)
|
|
{
|
|
if (currentModel != nullptr)
|
|
currentModel->menuItemSelected (commandId, topLevelIndex);
|
|
}
|
|
|
|
void addMenuItem (PopupMenu::MenuItemIterator& iter, NSMenu* menuToAddTo,
|
|
const int topLevelMenuId, const int topLevelIndex)
|
|
{
|
|
const PopupMenu::Item& i = iter.getItem();
|
|
NSString* text = juceStringToNS (i.text);
|
|
|
|
if (text == nil)
|
|
text = nsEmptyString();
|
|
|
|
if (i.isSeparator)
|
|
{
|
|
[menuToAddTo addItem: [NSMenuItem separatorItem]];
|
|
}
|
|
else if (i.isSectionHeader)
|
|
{
|
|
NSMenuItem* item = [menuToAddTo addItemWithTitle: text
|
|
action: nil
|
|
keyEquivalent: nsEmptyString()];
|
|
|
|
[item setEnabled: false];
|
|
}
|
|
else if (i.subMenu != nullptr)
|
|
{
|
|
if (recentItemsMenuName.isNotEmpty() && i.text == recentItemsMenuName)
|
|
{
|
|
if (recent == nullptr)
|
|
recent = std::make_unique<RecentFilesMenuItem>();
|
|
|
|
if (recent->recentItem != nil)
|
|
{
|
|
if (NSMenu* parent = [recent->recentItem menu])
|
|
[parent removeItem: recent->recentItem];
|
|
|
|
[menuToAddTo addItem: recent->recentItem];
|
|
return;
|
|
}
|
|
}
|
|
|
|
NSMenuItem* item = [menuToAddTo addItemWithTitle: text
|
|
action: nil
|
|
keyEquivalent: nsEmptyString()];
|
|
|
|
[item setTag: i.itemID];
|
|
[item setEnabled: i.isEnabled];
|
|
|
|
NSMenu* sub = createMenu (*i.subMenu, i.text, topLevelMenuId, topLevelIndex, false);
|
|
[menuToAddTo setSubmenu: sub forItem: item];
|
|
[sub release];
|
|
}
|
|
else
|
|
{
|
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
|
auto item = [[NSMenuItem alloc] initWithTitle: text
|
|
action: @selector (menuItemInvoked:)
|
|
keyEquivalent: nsEmptyString()];
|
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
|
|
|
[item setTag: topLevelIndex];
|
|
[item setEnabled: i.isEnabled];
|
|
#if defined (MAC_OS_X_VERSION_10_13) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_13
|
|
[item setState: i.isTicked ? NSControlStateValueOn : NSControlStateValueOff];
|
|
#else
|
|
[item setState: i.isTicked ? NSOnState : NSOffState];
|
|
#endif
|
|
[item setTarget: (id) callback];
|
|
|
|
auto* juceItem = new PopupMenu::Item (i);
|
|
juceItem->customComponent = nullptr;
|
|
|
|
[item setRepresentedObject: [createNSObjectFromJuceClass (juceItem) autorelease]];
|
|
|
|
if (i.commandManager != nullptr)
|
|
{
|
|
for (auto& kp : i.commandManager->getKeyMappings()->getKeyPressesAssignedToCommand (i.itemID))
|
|
{
|
|
if (kp != KeyPress::backspaceKey // (adding these is annoying because it flashes the menu bar
|
|
&& kp != KeyPress::deleteKey) // every time you press the key while editing text)
|
|
{
|
|
juce_wchar key = kp.getTextCharacter();
|
|
|
|
if (key == 0)
|
|
key = (juce_wchar) kp.getKeyCode();
|
|
|
|
[item setKeyEquivalent: juceStringToNS (String::charToString (key).toLowerCase())];
|
|
[item setKeyEquivalentModifierMask: juceModsToNSMods (kp.getModifiers())];
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
[menuToAddTo addItem: item];
|
|
[item release];
|
|
}
|
|
}
|
|
|
|
NSMenu* createMenu (const PopupMenu menu,
|
|
const String& menuName,
|
|
const int topLevelMenuId,
|
|
const int topLevelIndex,
|
|
const bool addDelegate)
|
|
{
|
|
NSMenu* m = [[NSMenu alloc] initWithTitle: juceStringToNS (menuName)];
|
|
|
|
[m setAutoenablesItems: false];
|
|
|
|
if (addDelegate)
|
|
[m setDelegate: (id<NSMenuDelegate>) callback];
|
|
|
|
for (PopupMenu::MenuItemIterator iter (menu); iter.next();)
|
|
addMenuItem (iter, m, topLevelMenuId, topLevelIndex);
|
|
|
|
[m update];
|
|
return m;
|
|
}
|
|
|
|
static JuceMainMenuHandler* instance;
|
|
|
|
MenuBarModel* currentModel = nullptr;
|
|
std::unique_ptr<PopupMenu> extraAppleMenuItems;
|
|
uint32 lastUpdateTime = 0;
|
|
NSObject* callback = nil;
|
|
String recentItemsMenuName;
|
|
bool isOpen = false, defferedUpdateRequested = false;
|
|
|
|
private:
|
|
struct RecentFilesMenuItem
|
|
{
|
|
RecentFilesMenuItem() : recentItem (nil)
|
|
{
|
|
if (NSNib* menuNib = [[[NSNib alloc] initWithNibNamed: @"RecentFilesMenuTemplate" bundle: nil] autorelease])
|
|
{
|
|
NSArray* array = nil;
|
|
|
|
#if (! defined (MAC_OS_X_VERSION_10_8)) || MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_8
|
|
[menuNib instantiateNibWithOwner: NSApp topLevelObjects: &array];
|
|
#else
|
|
[menuNib instantiateWithOwner: NSApp topLevelObjects: &array];
|
|
#endif
|
|
|
|
for (id object in array)
|
|
{
|
|
if ([object isKindOfClass: [NSMenu class]])
|
|
{
|
|
if (NSArray* items = [object itemArray])
|
|
{
|
|
if (NSMenuItem* item = findRecentFilesItem (items))
|
|
{
|
|
recentItem = [item retain];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
~RecentFilesMenuItem()
|
|
{
|
|
[recentItem release];
|
|
}
|
|
|
|
static NSMenuItem* findRecentFilesItem (NSArray* const items)
|
|
{
|
|
for (id object in items)
|
|
if (NSArray* subMenuItems = [[object submenu] itemArray])
|
|
for (id subObject in subMenuItems)
|
|
if ([subObject isKindOfClass: [NSMenuItem class]])
|
|
return subObject;
|
|
return nil;
|
|
}
|
|
|
|
NSMenuItem* recentItem;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RecentFilesMenuItem)
|
|
};
|
|
|
|
std::unique_ptr<RecentFilesMenuItem> recent;
|
|
|
|
//==============================================================================
|
|
static NSMenuItem* findMenuItemWithCommandID (NSMenu* const menu, int commandID)
|
|
{
|
|
for (NSInteger i = [menu numberOfItems]; --i >= 0;)
|
|
{
|
|
NSMenuItem* m = [menu itemAtIndex: i];
|
|
if (auto* menuItem = getJuceClassFromNSObject<PopupMenu::Item> ([m representedObject]))
|
|
if (menuItem->itemID == commandID)
|
|
return m;
|
|
|
|
if (NSMenu* sub = [m submenu])
|
|
if (NSMenuItem* found = findMenuItemWithCommandID (sub, commandID))
|
|
return found;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
static void flashMenuBar (NSMenu* menu)
|
|
{
|
|
if ([[menu title] isEqualToString: nsStringLiteral ("Apple")])
|
|
return;
|
|
|
|
[menu retain];
|
|
|
|
const unichar f35Key = NSF35FunctionKey;
|
|
NSString* f35String = [NSString stringWithCharacters: &f35Key length: 1];
|
|
|
|
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle: nsStringLiteral ("x")
|
|
action: nil
|
|
keyEquivalent: f35String];
|
|
[item setTarget: nil];
|
|
[menu insertItem: item atIndex: [menu numberOfItems]];
|
|
[item release];
|
|
|
|
if ([menu indexOfItem: item] >= 0)
|
|
{
|
|
NSEvent* f35Event = [NSEvent keyEventWithType: NSEventTypeKeyDown
|
|
location: NSZeroPoint
|
|
modifierFlags: NSEventModifierFlagCommand
|
|
timestamp: 0
|
|
windowNumber: 0
|
|
context: [NSGraphicsContext currentContext]
|
|
characters: f35String
|
|
charactersIgnoringModifiers: f35String
|
|
isARepeat: NO
|
|
keyCode: 0];
|
|
|
|
[menu performKeyEquivalent: f35Event];
|
|
|
|
if ([menu indexOfItem: item] >= 0)
|
|
[menu removeItem: item]; // (this throws if the item isn't actually in the menu)
|
|
}
|
|
|
|
[menu release];
|
|
}
|
|
|
|
static unsigned int juceModsToNSMods (const ModifierKeys mods)
|
|
{
|
|
unsigned int m = 0;
|
|
if (mods.isShiftDown()) m |= NSEventModifierFlagShift;
|
|
if (mods.isCtrlDown()) m |= NSEventModifierFlagControl;
|
|
if (mods.isAltDown()) m |= NSEventModifierFlagOption;
|
|
if (mods.isCommandDown()) m |= NSEventModifierFlagCommand;
|
|
return m;
|
|
}
|
|
|
|
// Apple Bug: For some reason [NSMenu removeAllItems] seems to leak it's objects
|
|
// on shutdown, so we need this method to release the items one-by-one manually
|
|
static void removeItemRecursive (NSMenu* parentMenu, int menuItemIndex)
|
|
{
|
|
if (isPositiveAndBelow (menuItemIndex, (int) [parentMenu numberOfItems]))
|
|
{
|
|
auto menuItem = [parentMenu itemAtIndex:menuItemIndex];
|
|
|
|
if (auto submenu = [menuItem submenu])
|
|
removeItemRecursive (submenu);
|
|
|
|
[parentMenu removeItem:menuItem];
|
|
}
|
|
else
|
|
jassertfalse;
|
|
}
|
|
|
|
static void removeItemRecursive (NSMenu* menu)
|
|
{
|
|
if (menu != nullptr)
|
|
{
|
|
auto n = static_cast<int> ([menu numberOfItems]);
|
|
|
|
for (auto i = n; --i >= 0;)
|
|
removeItemRecursive (menu, i);
|
|
}
|
|
}
|
|
|
|
static NSMenu* getMainMenuBar()
|
|
{
|
|
return JuceMainMenuBarHolder::getInstance()->mainMenuBar;
|
|
}
|
|
|
|
//==============================================================================
|
|
struct JuceMenuCallbackClass : public ObjCClass<NSObject>
|
|
{
|
|
JuceMenuCallbackClass() : ObjCClass ("JUCEMainMenu_")
|
|
{
|
|
addIvar<JuceMainMenuHandler*> ("owner");
|
|
|
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
|
addMethod (@selector (menuItemInvoked:), menuItemInvoked, "v@:@");
|
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
|
|
|
addMethod (@selector (menuNeedsUpdate:), menuNeedsUpdate, "v@:@");
|
|
|
|
addProtocol (@protocol (NSMenuDelegate));
|
|
|
|
registerClass();
|
|
}
|
|
|
|
static void setOwner (id self, JuceMainMenuHandler* owner)
|
|
{
|
|
object_setInstanceVariable (self, "owner", owner);
|
|
}
|
|
|
|
private:
|
|
static void menuItemInvoked (id self, SEL, NSMenuItem* item)
|
|
{
|
|
if (auto* juceItem = getJuceClassFromNSObject<PopupMenu::Item> ([item representedObject]))
|
|
getIvar<JuceMainMenuHandler*> (self, "owner")->invoke (*juceItem, static_cast<int> ([item tag]));
|
|
}
|
|
|
|
static void menuNeedsUpdate (id self, SEL, NSMenu* menu)
|
|
{
|
|
getIvar<JuceMainMenuHandler*> (self, "owner")->updateTopLevelMenu (menu);
|
|
}
|
|
};
|
|
};
|
|
|
|
JuceMainMenuHandler* JuceMainMenuHandler::instance = nullptr;
|
|
|
|
//==============================================================================
|
|
class TemporaryMainMenuWithStandardCommands
|
|
{
|
|
public:
|
|
explicit TemporaryMainMenuWithStandardCommands (FilePreviewComponent* filePreviewComponent)
|
|
: oldMenu (MenuBarModel::getMacMainMenu()), dummyModalComponent (filePreviewComponent)
|
|
{
|
|
if (auto* appleMenu = MenuBarModel::getMacExtraAppleItemsMenu())
|
|
oldAppleMenu = std::make_unique<PopupMenu> (*appleMenu);
|
|
|
|
if (auto* handler = JuceMainMenuHandler::instance)
|
|
oldRecentItems = handler->recentItemsMenuName;
|
|
|
|
MenuBarModel::setMacMainMenu (nullptr);
|
|
|
|
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
|
|
{
|
|
NSMenu* menu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Edit")];
|
|
NSMenuItem* item;
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Cut"), nil)
|
|
action: @selector (cut:) keyEquivalent: nsStringLiteral ("x")];
|
|
[menu addItem: item];
|
|
[item release];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Copy"), nil)
|
|
action: @selector (copy:) keyEquivalent: nsStringLiteral ("c")];
|
|
[menu addItem: item];
|
|
[item release];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Paste"), nil)
|
|
action: @selector (paste:) keyEquivalent: nsStringLiteral ("v")];
|
|
[menu addItem: item];
|
|
[item release];
|
|
|
|
editMenuIndex = [mainMenu numberOfItems];
|
|
|
|
item = [mainMenu addItemWithTitle: NSLocalizedString (nsStringLiteral ("Edit"), nil)
|
|
action: nil keyEquivalent: nsEmptyString()];
|
|
[mainMenu setSubmenu: menu forItem: item];
|
|
[menu release];
|
|
}
|
|
|
|
// use a dummy modal component so that apps can tell that something is currently modal.
|
|
dummyModalComponent.enterModalState (false);
|
|
}
|
|
|
|
~TemporaryMainMenuWithStandardCommands()
|
|
{
|
|
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
|
|
[mainMenu removeItemAtIndex:editMenuIndex];
|
|
|
|
MenuBarModel::setMacMainMenu (oldMenu, oldAppleMenu.get(), oldRecentItems);
|
|
}
|
|
|
|
static bool checkModalEvent (FilePreviewComponent* preview, const Component* targetComponent)
|
|
{
|
|
if (targetComponent == nullptr)
|
|
return false;
|
|
|
|
return (targetComponent == preview
|
|
|| targetComponent->findParentComponentOfClass<FilePreviewComponent>() != nullptr);
|
|
}
|
|
|
|
private:
|
|
MenuBarModel* const oldMenu = nullptr;
|
|
std::unique_ptr<PopupMenu> oldAppleMenu;
|
|
String oldRecentItems;
|
|
NSInteger editMenuIndex;
|
|
|
|
// The OS view already plays an alert when clicking outside
|
|
// the modal comp, so this override avoids adding extra
|
|
// inappropriate noises when the cancel button is pressed.
|
|
// This override is also important because it stops the base class
|
|
// calling ModalComponentManager::bringToFront, which can get
|
|
// recursive when file dialogs are involved
|
|
struct SilentDummyModalComp : public Component
|
|
{
|
|
explicit SilentDummyModalComp (FilePreviewComponent* p)
|
|
: preview (p) {}
|
|
|
|
void inputAttemptWhenModal() override {}
|
|
|
|
bool canModalEventBeSentToComponent (const Component* targetComponent) override
|
|
{
|
|
return checkModalEvent (preview, targetComponent);
|
|
}
|
|
|
|
FilePreviewComponent* preview = nullptr;
|
|
};
|
|
|
|
SilentDummyModalComp dummyModalComponent;
|
|
};
|
|
|
|
//==============================================================================
|
|
namespace MainMenuHelpers
|
|
{
|
|
static NSString* translateMenuName (const String& name)
|
|
{
|
|
return NSLocalizedString (juceStringToNS (TRANS (name)), nil);
|
|
}
|
|
|
|
static NSMenuItem* createMenuItem (NSMenu* menu, const String& name, SEL sel, NSString* key)
|
|
{
|
|
NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle: translateMenuName (name)
|
|
action: sel
|
|
keyEquivalent: key] autorelease];
|
|
[item setTarget: NSApp];
|
|
[menu addItem: item];
|
|
return item;
|
|
}
|
|
|
|
static void createStandardAppMenu (NSMenu* menu, const String& appName, const PopupMenu* extraItems)
|
|
{
|
|
if (extraItems != nullptr && JuceMainMenuHandler::instance != nullptr && extraItems->getNumItems() > 0)
|
|
{
|
|
for (PopupMenu::MenuItemIterator iter (*extraItems); iter.next();)
|
|
JuceMainMenuHandler::instance->addMenuItem (iter, menu, 0, -1);
|
|
|
|
[menu addItem: [NSMenuItem separatorItem]];
|
|
}
|
|
|
|
// Services...
|
|
NSMenuItem* services = [[[NSMenuItem alloc] initWithTitle: translateMenuName ("Services")
|
|
action: nil keyEquivalent: nsEmptyString()] autorelease];
|
|
[menu addItem: services];
|
|
|
|
NSMenu* servicesMenu = [[[NSMenu alloc] initWithTitle: translateMenuName ("Services")] autorelease];
|
|
[menu setSubmenu: servicesMenu forItem: services];
|
|
[NSApp setServicesMenu: servicesMenu];
|
|
[menu addItem: [NSMenuItem separatorItem]];
|
|
|
|
createMenuItem (menu, TRANS("Hide") + String (" ") + appName, @selector (hide:), nsStringLiteral ("h"));
|
|
|
|
[createMenuItem (menu, TRANS("Hide Others"), @selector (hideOtherApplications:), nsStringLiteral ("h"))
|
|
setKeyEquivalentModifierMask: NSEventModifierFlagCommand | NSEventModifierFlagOption];
|
|
|
|
createMenuItem (menu, TRANS("Show All"), @selector (unhideAllApplications:), nsEmptyString());
|
|
|
|
[menu addItem: [NSMenuItem separatorItem]];
|
|
|
|
createMenuItem (menu, TRANS("Quit") + String (" ") + appName, @selector (terminate:), nsStringLiteral ("q"));
|
|
}
|
|
|
|
// Since our app has no NIB, this initialises a standard app menu...
|
|
static void rebuildMainMenu (const PopupMenu* extraItems)
|
|
{
|
|
// this can't be used in a plugin!
|
|
jassert (JUCEApplicationBase::isStandaloneApp());
|
|
|
|
if (auto* app = JUCEApplicationBase::getInstance())
|
|
{
|
|
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
|
|
{
|
|
if ([mainMenu numberOfItems] > 0)
|
|
{
|
|
if (auto appMenu = [[mainMenu itemAtIndex: 0] submenu])
|
|
{
|
|
[appMenu removeAllItems];
|
|
MainMenuHelpers::createStandardAppMenu (appMenu, app->getApplicationName(), extraItems);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MenuBarModel::setMacMainMenu (MenuBarModel* newMenuBarModel,
|
|
const PopupMenu* extraAppleMenuItems,
|
|
const String& recentItemsMenuName)
|
|
{
|
|
if (getMacMainMenu() != newMenuBarModel)
|
|
{
|
|
JUCE_AUTORELEASEPOOL
|
|
{
|
|
if (newMenuBarModel == nullptr)
|
|
{
|
|
delete JuceMainMenuHandler::instance;
|
|
jassert (JuceMainMenuHandler::instance == nullptr); // should be zeroed in the destructor
|
|
jassert (extraAppleMenuItems == nullptr); // you can't specify some extra items without also supplying a model
|
|
|
|
extraAppleMenuItems = nullptr;
|
|
}
|
|
else
|
|
{
|
|
if (JuceMainMenuHandler::instance == nullptr)
|
|
JuceMainMenuHandler::instance = new JuceMainMenuHandler();
|
|
|
|
JuceMainMenuHandler::instance->setMenu (newMenuBarModel, extraAppleMenuItems, recentItemsMenuName);
|
|
}
|
|
}
|
|
}
|
|
|
|
MainMenuHelpers::rebuildMainMenu (extraAppleMenuItems);
|
|
|
|
if (newMenuBarModel != nullptr)
|
|
newMenuBarModel->menuItemsChanged();
|
|
}
|
|
|
|
MenuBarModel* MenuBarModel::getMacMainMenu()
|
|
{
|
|
if (auto* mm = JuceMainMenuHandler::instance)
|
|
return mm->currentModel;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const PopupMenu* MenuBarModel::getMacExtraAppleItemsMenu()
|
|
{
|
|
if (auto* mm = JuceMainMenuHandler::instance)
|
|
return mm->extraAppleMenuItems.get();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
using MenuTrackingChangedCallback = void (*)(bool);
|
|
extern MenuTrackingChangedCallback menuTrackingChangedCallback;
|
|
|
|
static void mainMenuTrackingChanged (bool isTracking)
|
|
{
|
|
PopupMenu::dismissAllActiveMenus();
|
|
|
|
if (auto* menuHandler = JuceMainMenuHandler::instance)
|
|
{
|
|
menuHandler->isOpen = isTracking;
|
|
|
|
if (auto* model = menuHandler->currentModel)
|
|
model->handleMenuBarActivate (isTracking);
|
|
|
|
if (menuHandler->defferedUpdateRequested && ! isTracking)
|
|
{
|
|
menuHandler->defferedUpdateRequested = false;
|
|
menuHandler->menuBarItemsChanged (menuHandler->currentModel);
|
|
}
|
|
}
|
|
}
|
|
|
|
void juce_initialiseMacMainMenu()
|
|
{
|
|
menuTrackingChangedCallback = mainMenuTrackingChanged;
|
|
|
|
if (JuceMainMenuHandler::instance == nullptr)
|
|
MainMenuHelpers::rebuildMainMenu (nullptr);
|
|
}
|
|
|
|
// (used from other modules that need to create an NSMenu)
|
|
NSMenu* createNSMenu (const PopupMenu&, const String&, int, int, bool);
|
|
NSMenu* createNSMenu (const PopupMenu& menu, const String& name, int topLevelMenuId, int topLevelIndex, bool addDelegate)
|
|
{
|
|
juce_initialiseMacMainMenu();
|
|
|
|
if (auto* mm = JuceMainMenuHandler::instance)
|
|
return mm->createMenu (menu, name, topLevelMenuId, topLevelIndex, addDelegate);
|
|
|
|
jassertfalse; // calling this before making sure the OSX main menu stuff was initialised?
|
|
return nil;
|
|
}
|
|
|
|
} // namespace juce
|