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.

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