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.

661 lines
24KB

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