The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

603 lines
22KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library - "Jules' Utility Class Extensions"
  4. Copyright 2004-11 by Raw Material Software Ltd.
  5. ------------------------------------------------------------------------------
  6. JUCE can be redistributed and/or modified under the terms of the GNU General
  7. Public License (Version 2), as published by the Free Software Foundation.
  8. A copy of the license is included in the JUCE distribution, or can be found
  9. online at www.gnu.org/licenses.
  10. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  11. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  13. ------------------------------------------------------------------------------
  14. To release a closed-source product which uses JUCE, commercial licenses are
  15. available: visit www.rawmaterialsoftware.com/juce for more information.
  16. ==============================================================================
  17. */
  18. class JuceMainMenuHandler : private MenuBarModel::Listener,
  19. private DeletedAtShutdown
  20. {
  21. public:
  22. JuceMainMenuHandler()
  23. : currentModel (nullptr),
  24. lastUpdateTime (0)
  25. {
  26. static JuceMenuCallbackClass cls;
  27. callback = [cls.createInstance() init];
  28. JuceMenuCallbackClass::setOwner (callback, this);
  29. }
  30. ~JuceMainMenuHandler()
  31. {
  32. setMenu (nullptr, nullptr);
  33. jassert (instance == this);
  34. instance = nullptr;
  35. [callback release];
  36. }
  37. void setMenu (MenuBarModel* const newMenuBarModel, const PopupMenu* newExtraAppleMenuItems)
  38. {
  39. if (currentModel != newMenuBarModel)
  40. {
  41. if (currentModel != nullptr)
  42. currentModel->removeListener (this);
  43. currentModel = newMenuBarModel;
  44. if (currentModel != nullptr)
  45. currentModel->addListener (this);
  46. menuBarItemsChanged (nullptr);
  47. }
  48. extraAppleMenuItems = createCopyIfNotNull (newExtraAppleMenuItems);
  49. }
  50. void addSubMenu (NSMenu* parent, const PopupMenu& child,
  51. const String& name, const int menuId, const int tag)
  52. {
  53. NSMenuItem* item = [parent addItemWithTitle: juceStringToNS (name)
  54. action: nil
  55. keyEquivalent: nsEmptyString()];
  56. [item setTag: tag];
  57. NSMenu* sub = createMenu (child, name, menuId, tag);
  58. [parent setSubmenu: sub forItem: item];
  59. [sub setAutoenablesItems: false];
  60. [sub release];
  61. }
  62. void updateSubMenu (NSMenuItem* parentItem, const PopupMenu& menuToCopy,
  63. const String& name, const int menuId, const int tag)
  64. {
  65. #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
  66. static bool is10_4 = (SystemStats::getOperatingSystemType() == SystemStats::MacOSX_10_4);
  67. if (is10_4)
  68. {
  69. [parentItem setTag: tag];
  70. NSMenu* menu = [parentItem submenu];
  71. [menu setTitle: juceStringToNS (name)];
  72. while ([menu numberOfItems] > 0)
  73. [menu removeItemAtIndex: 0];
  74. for (PopupMenu::MenuItemIterator iter (menuToCopy); iter.next();)
  75. addMenuItem (iter, menu, menuId, tag);
  76. [menu setAutoenablesItems: false];
  77. [menu update];
  78. return;
  79. }
  80. #endif
  81. // Note: This method used to update the contents of the existing menu in-place, but that caused
  82. // weird side-effects which messed-up keyboard focus when switching between windows. By creating
  83. // a new menu and replacing the old one with it, that problem seems to be avoided..
  84. NSMenu* menu = [[NSMenu alloc] initWithTitle: juceStringToNS (name)];
  85. for (PopupMenu::MenuItemIterator iter (menuToCopy); iter.next();)
  86. addMenuItem (iter, menu, menuId, tag);
  87. [menu setAutoenablesItems: false];
  88. [menu update];
  89. [parentItem setTag: tag];
  90. [parentItem setSubmenu: menu];
  91. [menu release];
  92. }
  93. void menuBarItemsChanged (MenuBarModel*)
  94. {
  95. lastUpdateTime = Time::getMillisecondCounter();
  96. StringArray menuNames;
  97. if (currentModel != nullptr)
  98. menuNames = currentModel->getMenuBarNames();
  99. NSMenu* menuBar = [NSApp mainMenu];
  100. while ([menuBar numberOfItems] > 1 + menuNames.size())
  101. [menuBar removeItemAtIndex: [menuBar numberOfItems] - 1];
  102. int menuId = 1;
  103. for (int i = 0; i < menuNames.size(); ++i)
  104. {
  105. const PopupMenu menu (currentModel->getMenuForIndex (i, menuNames [i]));
  106. if (i >= [menuBar numberOfItems] - 1)
  107. addSubMenu (menuBar, menu, menuNames[i], menuId, i);
  108. else
  109. updateSubMenu ([menuBar itemAtIndex: 1 + i], menu, menuNames[i], menuId, i);
  110. }
  111. }
  112. void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info)
  113. {
  114. NSMenuItem* item = findMenuItem ([NSApp mainMenu], info);
  115. if (item != nil)
  116. flashMenuBar ([item menu]);
  117. }
  118. void updateMenus (NSMenu* menu)
  119. {
  120. if (PopupMenu::dismissAllActiveMenus())
  121. {
  122. // If we were running a juce menu, then we should let that modal loop finish before allowing
  123. // the OS menus to start their own modal loop - so cancel the menu that was being opened..
  124. if ([menu respondsToSelector: @selector (cancelTracking)])
  125. [menu performSelector: @selector (cancelTracking)];
  126. }
  127. if (Time::getMillisecondCounter() > lastUpdateTime + 100)
  128. (new AsyncMenuUpdater())->post();
  129. }
  130. void invoke (const int commandId, ApplicationCommandManager* const commandManager, const int topLevelIndex) const
  131. {
  132. if (currentModel != nullptr)
  133. {
  134. if (commandManager != nullptr)
  135. {
  136. ApplicationCommandTarget::InvocationInfo info (commandId);
  137. info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
  138. commandManager->invoke (info, true);
  139. }
  140. (new AsyncCommandInvoker (commandId, topLevelIndex))->post();
  141. }
  142. }
  143. void invokeDirectly (const int commandId, const int topLevelIndex)
  144. {
  145. if (currentModel != nullptr)
  146. currentModel->menuItemSelected (commandId, topLevelIndex);
  147. }
  148. void addMenuItem (PopupMenu::MenuItemIterator& iter, NSMenu* menuToAddTo,
  149. const int topLevelMenuId, const int topLevelIndex)
  150. {
  151. NSString* text = juceStringToNS (iter.itemName.upToFirstOccurrenceOf ("<end>", false, true));
  152. if (text == nil)
  153. text = nsEmptyString();
  154. if (iter.isSeparator)
  155. {
  156. [menuToAddTo addItem: [NSMenuItem separatorItem]];
  157. }
  158. else if (iter.isSectionHeader)
  159. {
  160. NSMenuItem* item = [menuToAddTo addItemWithTitle: text
  161. action: nil
  162. keyEquivalent: nsEmptyString()];
  163. [item setEnabled: false];
  164. }
  165. else if (iter.subMenu != nullptr)
  166. {
  167. NSMenuItem* item = [menuToAddTo addItemWithTitle: text
  168. action: nil
  169. keyEquivalent: nsEmptyString()];
  170. [item setTag: iter.itemId];
  171. [item setEnabled: iter.isEnabled];
  172. NSMenu* sub = createMenu (*iter.subMenu, iter.itemName, topLevelMenuId, topLevelIndex);
  173. [sub setDelegate: nil];
  174. [menuToAddTo setSubmenu: sub forItem: item];
  175. [sub release];
  176. }
  177. else
  178. {
  179. NSMenuItem* item = [menuToAddTo addItemWithTitle: text
  180. action: @selector (menuItemInvoked:)
  181. keyEquivalent: nsEmptyString()];
  182. [item setTag: iter.itemId];
  183. [item setEnabled: iter.isEnabled];
  184. [item setState: iter.isTicked ? NSOnState : NSOffState];
  185. [item setTarget: (id) callback];
  186. NSMutableArray* info = [NSMutableArray arrayWithObject: [NSNumber numberWithUnsignedLongLong: (pointer_sized_int) (void*) iter.commandManager]];
  187. [info addObject: [NSNumber numberWithInt: topLevelIndex]];
  188. [item setRepresentedObject: info];
  189. if (iter.commandManager != nullptr)
  190. {
  191. const Array <KeyPress> keyPresses (iter.commandManager->getKeyMappings()
  192. ->getKeyPressesAssignedToCommand (iter.itemId));
  193. if (keyPresses.size() > 0)
  194. {
  195. const KeyPress& kp = keyPresses.getReference(0);
  196. if (kp.getKeyCode() != KeyPress::backspaceKey // (adding these is annoying because it flashes the menu bar
  197. && kp.getKeyCode() != KeyPress::deleteKey) // every time you press the key while editing text)
  198. {
  199. juce_wchar key = kp.getTextCharacter();
  200. if (key == 0)
  201. key = (juce_wchar) kp.getKeyCode();
  202. [item setKeyEquivalent: juceStringToNS (String::charToString (key).toLowerCase())];
  203. [item setKeyEquivalentModifierMask: juceModsToNSMods (kp.getModifiers())];
  204. }
  205. }
  206. }
  207. }
  208. }
  209. static JuceMainMenuHandler* instance;
  210. MenuBarModel* currentModel;
  211. ScopedPointer<PopupMenu> extraAppleMenuItems;
  212. uint32 lastUpdateTime;
  213. NSObject* callback;
  214. private:
  215. //==============================================================================
  216. NSMenu* createMenu (const PopupMenu menu,
  217. const String& menuName,
  218. const int topLevelMenuId,
  219. const int topLevelIndex)
  220. {
  221. NSMenu* m = [[NSMenu alloc] initWithTitle: juceStringToNS (menuName)];
  222. [m setAutoenablesItems: false];
  223. #if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
  224. [m setDelegate: (id<NSMenuDelegate>) callback];
  225. #else
  226. [m setDelegate: callback];
  227. #endif
  228. for (PopupMenu::MenuItemIterator iter (menu); iter.next();)
  229. addMenuItem (iter, m, topLevelMenuId, topLevelIndex);
  230. [m update];
  231. return m;
  232. }
  233. static NSMenuItem* findMenuItem (NSMenu* const menu, const ApplicationCommandTarget::InvocationInfo& info)
  234. {
  235. for (NSInteger i = [menu numberOfItems]; --i >= 0;)
  236. {
  237. NSMenuItem* m = [menu itemAtIndex: i];
  238. if ([m tag] == info.commandID)
  239. return m;
  240. if ([m submenu] != nil)
  241. {
  242. NSMenuItem* found = findMenuItem ([m submenu], info);
  243. if (found != nil)
  244. return found;
  245. }
  246. }
  247. return nil;
  248. }
  249. static void flashMenuBar (NSMenu* menu)
  250. {
  251. if ([[menu title] isEqualToString: nsStringLiteral ("Apple")])
  252. return;
  253. [menu retain];
  254. const unichar f35Key = NSF35FunctionKey;
  255. NSString* f35String = [NSString stringWithCharacters: &f35Key length: 1];
  256. NSMenuItem* item = [[NSMenuItem alloc] initWithTitle: nsStringLiteral ("x")
  257. action: nil
  258. keyEquivalent: f35String];
  259. [item setTarget: nil];
  260. [menu insertItem: item atIndex: [menu numberOfItems]];
  261. [item release];
  262. if ([menu indexOfItem: item] >= 0)
  263. {
  264. NSEvent* f35Event = [NSEvent keyEventWithType: NSKeyDown
  265. location: NSZeroPoint
  266. modifierFlags: NSCommandKeyMask
  267. timestamp: 0
  268. windowNumber: 0
  269. context: [NSGraphicsContext currentContext]
  270. characters: f35String
  271. charactersIgnoringModifiers: f35String
  272. isARepeat: NO
  273. keyCode: 0];
  274. [menu performKeyEquivalent: f35Event];
  275. if ([menu indexOfItem: item] >= 0)
  276. [menu removeItem: item]; // (this throws if the item isn't actually in the menu)
  277. }
  278. [menu release];
  279. }
  280. static unsigned int juceModsToNSMods (const ModifierKeys& mods)
  281. {
  282. unsigned int m = 0;
  283. if (mods.isShiftDown()) m |= NSShiftKeyMask;
  284. if (mods.isCtrlDown()) m |= NSControlKeyMask;
  285. if (mods.isAltDown()) m |= NSAlternateKeyMask;
  286. if (mods.isCommandDown()) m |= NSCommandKeyMask;
  287. return m;
  288. }
  289. class AsyncMenuUpdater : public CallbackMessage
  290. {
  291. public:
  292. AsyncMenuUpdater() {}
  293. void messageCallback()
  294. {
  295. if (instance != nullptr)
  296. instance->menuBarItemsChanged (nullptr);
  297. }
  298. private:
  299. JUCE_DECLARE_NON_COPYABLE (AsyncMenuUpdater);
  300. };
  301. class AsyncCommandInvoker : public CallbackMessage
  302. {
  303. public:
  304. AsyncCommandInvoker (const int commandId_, const int topLevelIndex_)
  305. : commandId (commandId_), topLevelIndex (topLevelIndex_)
  306. {}
  307. void messageCallback()
  308. {
  309. if (instance != nullptr)
  310. instance->invokeDirectly (commandId, topLevelIndex);
  311. }
  312. private:
  313. const int commandId, topLevelIndex;
  314. JUCE_DECLARE_NON_COPYABLE (AsyncCommandInvoker);
  315. };
  316. //==============================================================================
  317. struct JuceMenuCallbackClass : public ObjCClass <NSObject>
  318. {
  319. JuceMenuCallbackClass() : ObjCClass <NSObject> ("JUCEMainMenu_")
  320. {
  321. addIvar<JuceMainMenuHandler*> ("owner");
  322. addMethod (@selector (menuItemInvoked:), menuItemInvoked, "v@:@");
  323. addMethod (@selector (menuNeedsUpdate:), menuNeedsUpdate, "v@:@");
  324. #if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
  325. addProtocol (@protocol (NSMenuDelegate));
  326. #endif
  327. registerClass();
  328. }
  329. static void setOwner (id self, JuceMainMenuHandler* owner)
  330. {
  331. object_setInstanceVariable (self, "owner", owner);
  332. }
  333. private:
  334. static void menuItemInvoked (id self, SEL, id menu)
  335. {
  336. JuceMainMenuHandler* const owner = getIvar<JuceMainMenuHandler*> (self, "owner");
  337. NSMenuItem* item = (NSMenuItem*) menu;
  338. if ([[item representedObject] isKindOfClass: [NSArray class]])
  339. {
  340. // 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
  341. // our own components, which may have wanted to intercept it. So, rather than dispatching directly, we'll feed it back
  342. // into the focused component and let it trigger the menu item indirectly.
  343. NSEvent* e = [NSApp currentEvent];
  344. if ([e type] == NSKeyDown || [e type] == NSKeyUp)
  345. {
  346. if (juce::Component::getCurrentlyFocusedComponent() != nullptr)
  347. {
  348. juce::NSViewComponentPeer* peer = dynamic_cast <juce::NSViewComponentPeer*> (juce::Component::getCurrentlyFocusedComponent()->getPeer());
  349. if (peer != nullptr)
  350. {
  351. if ([e type] == NSKeyDown)
  352. peer->redirectKeyDown (e);
  353. else
  354. peer->redirectKeyUp (e);
  355. return;
  356. }
  357. }
  358. }
  359. NSArray* info = (NSArray*) [item representedObject];
  360. owner->invoke ((int) [item tag],
  361. (ApplicationCommandManager*) (pointer_sized_int)
  362. [((NSNumber*) [info objectAtIndex: 0]) unsignedLongLongValue],
  363. (int) [((NSNumber*) [info objectAtIndex: 1]) intValue]);
  364. }
  365. }
  366. static void menuNeedsUpdate (id self, SEL, NSMenu* menu)
  367. {
  368. if (instance != nullptr)
  369. instance->updateMenus (menu);
  370. }
  371. };
  372. };
  373. JuceMainMenuHandler* JuceMainMenuHandler::instance = nullptr;
  374. //==============================================================================
  375. namespace MainMenuHelpers
  376. {
  377. static NSString* translateMenuName (const String& name)
  378. {
  379. return NSLocalizedString (juceStringToNS (TRANS (name)), nil);
  380. }
  381. static NSMenuItem* createMenuItem (NSMenu* menu, const String& name, SEL sel, NSString* key)
  382. {
  383. NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle: translateMenuName (name)
  384. action: sel
  385. keyEquivalent: key] autorelease];
  386. [item setTarget: NSApp];
  387. [menu addItem: item];
  388. return item;
  389. }
  390. static void createStandardAppMenu (NSMenu* menu, const String& appName, const PopupMenu* extraItems)
  391. {
  392. if (extraItems != nullptr && JuceMainMenuHandler::instance != nullptr && extraItems->getNumItems() > 0)
  393. {
  394. for (PopupMenu::MenuItemIterator iter (*extraItems); iter.next();)
  395. JuceMainMenuHandler::instance->addMenuItem (iter, menu, 0, -1);
  396. [menu addItem: [NSMenuItem separatorItem]];
  397. }
  398. // Services...
  399. NSMenuItem* services = [[[NSMenuItem alloc] initWithTitle: translateMenuName ("Services")
  400. action: nil keyEquivalent: nsEmptyString()] autorelease];
  401. [menu addItem: services];
  402. NSMenu* servicesMenu = [[[NSMenu alloc] initWithTitle: translateMenuName ("Services")] autorelease];
  403. [menu setSubmenu: servicesMenu forItem: services];
  404. [NSApp setServicesMenu: servicesMenu];
  405. [menu addItem: [NSMenuItem separatorItem]];
  406. createMenuItem (menu, "Hide " + appName, @selector (hide:), nsStringLiteral ("h"));
  407. [createMenuItem (menu, "Hide Others", @selector (hideOtherApplications:), nsStringLiteral ("h"))
  408. setKeyEquivalentModifierMask: NSCommandKeyMask | NSAlternateKeyMask];
  409. createMenuItem (menu, "Show All", @selector (unhideAllApplications:), nsEmptyString());
  410. [menu addItem: [NSMenuItem separatorItem]];
  411. createMenuItem (menu, "Quit " + appName, @selector (terminate:), nsStringLiteral ("q"));
  412. }
  413. // Since our app has no NIB, this initialises a standard app menu...
  414. static void rebuildMainMenu (const PopupMenu* extraItems)
  415. {
  416. // this can't be used in a plugin!
  417. jassert (JUCEApplication::isStandaloneApp());
  418. if (JUCEApplication::getInstance() != nullptr)
  419. {
  420. JUCE_AUTORELEASEPOOL
  421. NSMenu* mainMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("MainMenu")];
  422. NSMenuItem* item = [mainMenu addItemWithTitle: nsStringLiteral ("Apple") action: nil keyEquivalent: nsEmptyString()];
  423. NSMenu* appMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Apple")];
  424. [NSApp performSelector: @selector (setAppleMenu:) withObject: appMenu];
  425. [mainMenu setSubmenu: appMenu forItem: item];
  426. [NSApp setMainMenu: mainMenu];
  427. MainMenuHelpers::createStandardAppMenu (appMenu, JUCEApplication::getInstance()->getApplicationName(), extraItems);
  428. [appMenu release];
  429. [mainMenu release];
  430. }
  431. }
  432. }
  433. void MenuBarModel::setMacMainMenu (MenuBarModel* newMenuBarModel,
  434. const PopupMenu* extraAppleMenuItems)
  435. {
  436. if (getMacMainMenu() != newMenuBarModel)
  437. {
  438. JUCE_AUTORELEASEPOOL
  439. if (newMenuBarModel == nullptr)
  440. {
  441. delete JuceMainMenuHandler::instance;
  442. jassert (JuceMainMenuHandler::instance == nullptr); // should be zeroed in the destructor
  443. jassert (extraAppleMenuItems == nullptr); // you can't specify some extra items without also supplying a model
  444. extraAppleMenuItems = nullptr;
  445. }
  446. else
  447. {
  448. if (JuceMainMenuHandler::instance == nullptr)
  449. JuceMainMenuHandler::instance = new JuceMainMenuHandler();
  450. JuceMainMenuHandler::instance->setMenu (newMenuBarModel, extraAppleMenuItems);
  451. }
  452. }
  453. MainMenuHelpers::rebuildMainMenu (extraAppleMenuItems);
  454. if (newMenuBarModel != nullptr)
  455. newMenuBarModel->menuItemsChanged();
  456. }
  457. MenuBarModel* MenuBarModel::getMacMainMenu()
  458. {
  459. return JuceMainMenuHandler::instance != nullptr
  460. ? JuceMainMenuHandler::instance->currentModel : nullptr;
  461. }
  462. const PopupMenu* MenuBarModel::getMacExtraAppleItemsMenu()
  463. {
  464. return JuceMainMenuHandler::instance != nullptr
  465. ? JuceMainMenuHandler::instance->extraAppleMenuItems.get() : nullptr;
  466. }
  467. typedef void (*MenuTrackingBeganCallback)();
  468. extern MenuTrackingBeganCallback menuTrackingBeganCallback;
  469. static void mainMenuTrackingBegan()
  470. {
  471. PopupMenu::dismissAllActiveMenus();
  472. }
  473. void juce_initialiseMacMainMenu()
  474. {
  475. menuTrackingBeganCallback = mainMenuTrackingBegan;
  476. if (JuceMainMenuHandler::instance == nullptr)
  477. MainMenuHelpers::rebuildMainMenu (nullptr);
  478. }