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.

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