2018-06-17 20:34:53 +08:00
|
|
|
/*
|
|
|
|
==============================================================================
|
|
|
|
|
|
|
|
This file is part of the JUCE library.
|
|
|
|
Copyright (c) 2017 - ROLI Ltd.
|
|
|
|
|
|
|
|
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 5 End-User License
|
|
|
|
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
|
|
|
27th April 2017).
|
|
|
|
|
|
|
|
End User License Agreement: www.juce.com/juce-5-licence
|
|
|
|
Privacy Policy: www.juce.com/juce-5-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")])
|
|
|
|
{
|
2019-06-23 03:41:38 +08:00
|
|
|
auto item = [mainMenuBar addItemWithTitle: nsStringLiteral ("Apple")
|
|
|
|
action: nil
|
2018-06-17 20:34:53 +08:00
|
|
|
keyEquivalent: nsEmptyString()];
|
|
|
|
|
2019-06-23 03:41:38 +08:00
|
|
|
auto appMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Apple")];
|
2018-06-17 20:34:53 +08:00
|
|
|
|
|
|
|
[NSApp performSelector: @selector (setAppleMenu:) withObject: appMenu];
|
|
|
|
[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);
|
|
|
|
}
|
|
|
|
|
2019-06-23 03:41:38 +08:00
|
|
|
~JuceMainMenuHandler() override
|
2018-06-17 20:34:53 +08:00
|
|
|
{
|
|
|
|
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.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 (i.text == recentItemsMenuName)
|
|
|
|
{
|
|
|
|
if (recent == nullptr)
|
|
|
|
recent.reset (new 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
|
|
|
|
{
|
2019-06-23 03:41:38 +08:00
|
|
|
auto item = [[NSMenuItem alloc] initWithTitle: text
|
|
|
|
action: @selector (menuItemInvoked:)
|
|
|
|
keyEquivalent: nsEmptyString()];
|
2018-06-17 20:34:53 +08:00
|
|
|
|
|
|
|
[item setTag: topLevelIndex];
|
|
|
|
[item setEnabled: i.isEnabled];
|
|
|
|
[item setState: i.isTicked ? NSOnState : NSOffState];
|
|
|
|
[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)
|
|
|
|
{
|
|
|
|
#if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
|
|
|
|
[m setDelegate: (id<NSMenuDelegate>) callback];
|
|
|
|
#else
|
|
|
|
[m setDelegate: callback];
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
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]))
|
|
|
|
{
|
2019-06-23 03:41:38 +08:00
|
|
|
auto menuItem = [parentMenu itemAtIndex:menuItemIndex];
|
2018-06-17 20:34:53 +08:00
|
|
|
|
2019-06-23 03:41:38 +08:00
|
|
|
if (auto submenu = [menuItem submenu])
|
2018-06-17 20:34:53 +08:00
|
|
|
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<NSObject> ("JUCEMainMenu_")
|
|
|
|
{
|
|
|
|
addIvar<JuceMainMenuHandler*> ("owner");
|
|
|
|
|
|
|
|
addMethod (@selector (menuItemInvoked:), menuItemInvoked, "v@:@");
|
|
|
|
addMethod (@selector (menuNeedsUpdate:), menuNeedsUpdate, "v@:@");
|
|
|
|
|
|
|
|
#if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
|
|
|
|
addProtocol (@protocol (NSMenuDelegate));
|
|
|
|
#endif
|
|
|
|
|
|
|
|
registerClass();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void setOwner (id self, JuceMainMenuHandler* owner)
|
|
|
|
{
|
|
|
|
object_setInstanceVariable (self, "owner", owner);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
static void menuItemInvoked (id self, SEL, NSMenuItem* item)
|
|
|
|
{
|
|
|
|
auto owner = getIvar<JuceMainMenuHandler*> (self, "owner");
|
|
|
|
|
|
|
|
if (auto* juceItem = getJuceClassFromNSObject<PopupMenu::Item> ([item representedObject]))
|
|
|
|
{
|
|
|
|
// If the menu is being triggered by a keypress, the OS will have picked it up before we had a chance to offer it to
|
|
|
|
// our own components, which may have wanted to intercept it. So, rather than dispatching directly, we'll feed it back
|
|
|
|
// into the focused component and let it trigger the menu item indirectly.
|
|
|
|
NSEvent* e = [NSApp currentEvent];
|
|
|
|
|
|
|
|
if ([e type] == NSEventTypeKeyDown || [e type] == NSEventTypeKeyUp)
|
|
|
|
{
|
|
|
|
if (auto* focused = juce::Component::getCurrentlyFocusedComponent())
|
|
|
|
{
|
|
|
|
if (auto peer = dynamic_cast<juce::NSViewComponentPeer*> (focused->getPeer()))
|
|
|
|
{
|
|
|
|
if ([e type] == NSEventTypeKeyDown)
|
|
|
|
peer->redirectKeyDown (e);
|
|
|
|
else
|
|
|
|
peer->redirectKeyUp (e);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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:
|
|
|
|
TemporaryMainMenuWithStandardCommands()
|
|
|
|
: oldMenu (MenuBarModel::getMacMainMenu())
|
|
|
|
{
|
|
|
|
if (auto* appleMenu = MenuBarModel::getMacExtraAppleItemsMenu())
|
|
|
|
oldAppleMenu.reset (new 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
MenuBarModel* const oldMenu;
|
|
|
|
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
|
|
|
|
{
|
|
|
|
SilentDummyModalComp() {}
|
|
|
|
void inputAttemptWhenModal() override {}
|
|
|
|
};
|
|
|
|
|
|
|
|
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)
|
|
|
|
{
|
2019-06-23 03:41:38 +08:00
|
|
|
if (auto appMenu = [[mainMenu itemAtIndex: 0] submenu])
|
2018-06-17 20:34:53 +08:00
|
|
|
{
|
|
|
|
[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;
|
|
|
|
}
|
|
|
|
|
2019-06-23 03:41:38 +08:00
|
|
|
using MenuTrackingChangedCallback = void (*)(bool);
|
2018-06-17 20:34:53 +08:00
|
|
|
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
|