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