/* ============================================================================== This file is part of the JUCE library - "Jules' Utility Class Extensions" Copyright 2004-11 by Raw Material Software Ltd. ------------------------------------------------------------------------------ JUCE can be redistributed and/or modified under the terms of the GNU General Public License (Version 2), as published by the Free Software Foundation. A copy of the license is included in the JUCE distribution, or can be found online at www.gnu.org/licenses. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.rawmaterialsoftware.com/juce for more information. ============================================================================== */ class JuceMainMenuHandler : private MenuBarModel::Listener, private DeletedAtShutdown { public: JuceMainMenuHandler() : currentModel (nullptr), lastUpdateTime (0) { static JuceMenuCallbackClass cls; callback = [cls.createInstance() init]; JuceMenuCallbackClass::setOwner (callback, this); } ~JuceMainMenuHandler() { setMenu (nullptr, nullptr); jassert (instance == this); instance = nullptr; [callback release]; } void setMenu (MenuBarModel* const newMenuBarModel, const PopupMenu* newExtraAppleMenuItems) { if (currentModel != newMenuBarModel) { if (currentModel != nullptr) currentModel->removeListener (this); currentModel = newMenuBarModel; if (currentModel != nullptr) currentModel->addListener (this); menuBarItemsChanged (nullptr); } extraAppleMenuItems = newExtraAppleMenuItems != nullptr ? new PopupMenu (*newExtraAppleMenuItems) : nullptr; } void addSubMenu (NSMenu* parent, const PopupMenu& child, const String& name, const int menuId, const int tag) { NSMenuItem* item = [parent addItemWithTitle: juceStringToNS (name) action: nil keyEquivalent: nsEmptyString()]; [item setTag: tag]; NSMenu* sub = createMenu (child, name, menuId, tag); [parent setSubmenu: sub forItem: item]; [sub setAutoenablesItems: false]; [sub release]; } void updateSubMenu (NSMenuItem* parentItem, const PopupMenu& menuToCopy, const String& name, const int menuId, const int tag) { #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 static bool is10_4 = (SystemStats::getOSXMinorVersionNumber() <= 4); if (is10_4) { [parentItem setTag: tag]; NSMenu* menu = [parentItem submenu]; [menu setTitle: juceStringToNS (name)]; while ([menu numberOfItems] > 0) [menu removeItemAtIndex: 0]; for (PopupMenu::MenuItemIterator iter (menuToCopy); iter.next();) addMenuItem (iter, menu, menuId, tag); [menu setAutoenablesItems: false]; [menu update]; return; } #endif // 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, tag); [menu setAutoenablesItems: false]; [menu update]; [parentItem setTag: tag]; [parentItem setSubmenu: menu]; [menu release]; } void menuBarItemsChanged (MenuBarModel*) { lastUpdateTime = Time::getMillisecondCounter(); StringArray menuNames; if (currentModel != nullptr) menuNames = currentModel->getMenuBarNames(); NSMenu* menuBar = [NSApp mainMenu]; while ([menuBar numberOfItems] > 1 + menuNames.size()) [menuBar removeItemAtIndex: [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) addSubMenu (menuBar, menu, menuNames[i], menuId, i); else updateSubMenu ([menuBar itemAtIndex: 1 + i], menu, menuNames[i], menuId, i); } } void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info) { NSMenuItem* item = findMenuItem ([NSApp mainMenu], info); if (item != nil) flashMenuBar ([item menu]); } void updateMenus (NSMenu* menu) { if (PopupMenu::dismissAllActiveMenus()) { // If we were running a juce menu, then we should let that modal loop finish before allowing // the OS menus to start their own modal loop - so cancel the menu that was being opened.. if ([menu respondsToSelector: @selector (cancelTracking)]) [menu performSelector: @selector (cancelTracking)]; } if (Time::getMillisecondCounter() > lastUpdateTime + 100) (new AsyncMenuUpdater())->post(); } void invoke (const int commandId, ApplicationCommandManager* const commandManager, const int topLevelIndex) const { if (currentModel != nullptr) { if (commandManager != nullptr) { ApplicationCommandTarget::InvocationInfo info (commandId); info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu; commandManager->invoke (info, true); } (new AsyncCommandInvoker (commandId, topLevelIndex))->post(); } } void invokeDirectly (const int commandId, const int topLevelIndex) { if (currentModel != nullptr) currentModel->menuItemSelected (commandId, topLevelIndex); } void addMenuItem (PopupMenu::MenuItemIterator& iter, NSMenu* menuToAddTo, const int topLevelMenuId, const int topLevelIndex) { NSString* text = juceStringToNS (iter.itemName.upToFirstOccurrenceOf ("", false, true)); if (text == nil) text = nsEmptyString(); if (iter.isSeparator) { [menuToAddTo addItem: [NSMenuItem separatorItem]]; } else if (iter.isSectionHeader) { NSMenuItem* item = [menuToAddTo addItemWithTitle: text action: nil keyEquivalent: nsEmptyString()]; [item setEnabled: false]; } else if (iter.subMenu != nullptr) { NSMenuItem* item = [menuToAddTo addItemWithTitle: text action: nil keyEquivalent: nsEmptyString()]; [item setTag: iter.itemId]; [item setEnabled: iter.isEnabled]; NSMenu* sub = createMenu (*iter.subMenu, iter.itemName, topLevelMenuId, topLevelIndex); [sub setDelegate: nil]; [menuToAddTo setSubmenu: sub forItem: item]; [sub release]; } else { NSMenuItem* item = [menuToAddTo addItemWithTitle: text action: @selector (menuItemInvoked:) keyEquivalent: nsEmptyString()]; [item setTag: iter.itemId]; [item setEnabled: iter.isEnabled]; [item setState: iter.isTicked ? NSOnState : NSOffState]; [item setTarget: (id) callback]; NSMutableArray* info = [NSMutableArray arrayWithObject: [NSNumber numberWithUnsignedLongLong: (pointer_sized_int) (void*) iter.commandManager]]; [info addObject: [NSNumber numberWithInt: topLevelIndex]]; [item setRepresentedObject: info]; if (iter.commandManager != nullptr) { const Array keyPresses (iter.commandManager->getKeyMappings() ->getKeyPressesAssignedToCommand (iter.itemId)); if (keyPresses.size() > 0) { const KeyPress& kp = keyPresses.getReference(0); if (kp.getKeyCode() != KeyPress::backspaceKey // (adding these is annoying because it flashes the menu bar && kp.getKeyCode() != 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())]; } } } } } static JuceMainMenuHandler* instance; MenuBarModel* currentModel; ScopedPointer extraAppleMenuItems; uint32 lastUpdateTime; NSObject* callback; private: //============================================================================== NSMenu* createMenu (const PopupMenu menu, const String& menuName, const int topLevelMenuId, const int topLevelIndex) { NSMenu* m = [[NSMenu alloc] initWithTitle: juceStringToNS (menuName)]; [m setAutoenablesItems: false]; #if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 [m setDelegate: (id) callback]; #else [m setDelegate: callback]; #endif for (PopupMenu::MenuItemIterator iter (menu); iter.next();) addMenuItem (iter, m, topLevelMenuId, topLevelIndex); [m update]; return m; } static NSMenuItem* findMenuItem (NSMenu* const menu, const ApplicationCommandTarget::InvocationInfo& info) { for (NSInteger i = [menu numberOfItems]; --i >= 0;) { NSMenuItem* m = [menu itemAtIndex: i]; if ([m tag] == info.commandID) return m; if ([m submenu] != nil) { NSMenuItem* found = findMenuItem ([m submenu], info); if (found != nil) 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: NSKeyDown location: NSZeroPoint modifierFlags: NSCommandKeyMask 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 |= NSShiftKeyMask; if (mods.isCtrlDown()) m |= NSControlKeyMask; if (mods.isAltDown()) m |= NSAlternateKeyMask; if (mods.isCommandDown()) m |= NSCommandKeyMask; return m; } class AsyncMenuUpdater : public CallbackMessage { public: AsyncMenuUpdater() {} void messageCallback() { if (instance != nullptr) instance->menuBarItemsChanged (nullptr); } private: JUCE_DECLARE_NON_COPYABLE (AsyncMenuUpdater); }; class AsyncCommandInvoker : public CallbackMessage { public: AsyncCommandInvoker (const int commandId_, const int topLevelIndex_) : commandId (commandId_), topLevelIndex (topLevelIndex_) {} void messageCallback() { if (instance != nullptr) instance->invokeDirectly (commandId, topLevelIndex); } private: const int commandId, topLevelIndex; JUCE_DECLARE_NON_COPYABLE (AsyncCommandInvoker); }; //============================================================================== struct JuceMenuCallbackClass : public ObjCClass { JuceMenuCallbackClass() : ObjCClass ("JUCEMainMenu_") { addIvar ("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, id menu) { JuceMainMenuHandler* const owner = getIvar (self, "owner"); NSMenuItem* item = (NSMenuItem*) menu; if ([[item representedObject] isKindOfClass: [NSArray class]]) { // 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] == NSKeyDown || [e type] == NSKeyUp) { if (juce::Component::getCurrentlyFocusedComponent() != nullptr) { juce::NSViewComponentPeer* peer = dynamic_cast (juce::Component::getCurrentlyFocusedComponent()->getPeer()); if (peer != nullptr) { if ([e type] == NSKeyDown) peer->redirectKeyDown (e); else peer->redirectKeyUp (e); return; } } } NSArray* info = (NSArray*) [item representedObject]; owner->invoke ((int) [item tag], (ApplicationCommandManager*) (pointer_sized_int) [((NSNumber*) [info objectAtIndex: 0]) unsignedLongLongValue], (int) [((NSNumber*) [info objectAtIndex: 1]) intValue]); } } static void menuNeedsUpdate (id self, SEL, NSMenu* menu) { if (instance != nullptr) instance->updateMenus (menu); } }; }; JuceMainMenuHandler* JuceMainMenuHandler::instance = nullptr; //============================================================================== namespace MainMenuHelpers { static NSMenu* 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]]; } NSMenuItem* item; // Services... item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Services"), nil) action: nil keyEquivalent: nsEmptyString()]; [menu addItem: item]; [item release]; NSMenu* servicesMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Services")]; [menu setSubmenu: servicesMenu forItem: item]; [NSApp setServicesMenu: servicesMenu]; [servicesMenu release]; [menu addItem: [NSMenuItem separatorItem]]; // Hide + Show stuff... item = [[NSMenuItem alloc] initWithTitle: juceStringToNS ("Hide " + appName) action: @selector (hide:) keyEquivalent: nsStringLiteral ("h")]; [item setTarget: NSApp]; [menu addItem: item]; [item release]; item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Hide Others"), nil) action: @selector (hideOtherApplications:) keyEquivalent: nsStringLiteral ("h")]; [item setKeyEquivalentModifierMask: NSCommandKeyMask | NSAlternateKeyMask]; [item setTarget: NSApp]; [menu addItem: item]; [item release]; item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Show All"), nil) action: @selector (unhideAllApplications:) keyEquivalent: nsEmptyString()]; [item setTarget: NSApp]; [menu addItem: item]; [item release]; [menu addItem: [NSMenuItem separatorItem]]; // Quit item.... item = [[NSMenuItem alloc] initWithTitle: juceStringToNS ("Quit " + appName) action: @selector (terminate:) keyEquivalent: nsStringLiteral ("q")]; [item setTarget: NSApp]; [menu addItem: item]; [item release]; return menu; } // 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 (JUCEApplication::isStandaloneApp()); if (JUCEApplication::getInstance() != nullptr) { JUCE_AUTORELEASEPOOL NSMenu* mainMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("MainMenu")]; NSMenuItem* item = [mainMenu addItemWithTitle: nsStringLiteral ("Apple") action: nil keyEquivalent: nsEmptyString()]; NSMenu* appMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Apple")]; [NSApp performSelector: @selector (setAppleMenu:) withObject: appMenu]; [mainMenu setSubmenu: appMenu forItem: item]; [NSApp setMainMenu: mainMenu]; MainMenuHelpers::createStandardAppMenu (appMenu, JUCEApplication::getInstance()->getApplicationName(), extraItems); [appMenu release]; [mainMenu release]; } } } void MenuBarModel::setMacMainMenu (MenuBarModel* newMenuBarModel, const PopupMenu* extraAppleMenuItems) { 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); } } MainMenuHelpers::rebuildMainMenu (extraAppleMenuItems); if (newMenuBarModel != nullptr) newMenuBarModel->menuItemsChanged(); } MenuBarModel* MenuBarModel::getMacMainMenu() { return JuceMainMenuHandler::instance != nullptr ? JuceMainMenuHandler::instance->currentModel : nullptr; } const PopupMenu* MenuBarModel::getMacExtraAppleItemsMenu() { return JuceMainMenuHandler::instance != nullptr ? JuceMainMenuHandler::instance->extraAppleMenuItems.get() : nullptr; } void juce_initialiseMacMainMenu() { if (JuceMainMenuHandler::instance == nullptr) MainMenuHelpers::rebuildMainMenu (nullptr); }