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.

613 lines
23KB

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