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.

833 lines
30KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2020 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 6 End-User License
  8. Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
  9. End User License Agreement: www.juce.com/juce-6-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. //==============================================================================
  21. struct JuceMainMenuBarHolder : private DeletedAtShutdown
  22. {
  23. JuceMainMenuBarHolder()
  24. : mainMenuBar ([[NSMenu alloc] initWithTitle: nsStringLiteral ("MainMenu")])
  25. {
  26. auto item = [mainMenuBar addItemWithTitle: nsStringLiteral ("Apple")
  27. action: nil
  28. keyEquivalent: nsEmptyString()];
  29. auto appMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Apple")];
  30. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
  31. [NSApp performSelector: @selector (setAppleMenu:) withObject: appMenu];
  32. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  33. [mainMenuBar setSubmenu: appMenu forItem: item];
  34. [appMenu release];
  35. [NSApp setMainMenu: mainMenuBar];
  36. }
  37. ~JuceMainMenuBarHolder()
  38. {
  39. clearSingletonInstance();
  40. [NSApp setMainMenu: nil];
  41. [mainMenuBar release];
  42. }
  43. NSMenu* mainMenuBar = nil;
  44. JUCE_DECLARE_SINGLETON_SINGLETHREADED (JuceMainMenuBarHolder, true)
  45. };
  46. JUCE_IMPLEMENT_SINGLETON (JuceMainMenuBarHolder)
  47. //==============================================================================
  48. class JuceMainMenuHandler : private MenuBarModel::Listener,
  49. private DeletedAtShutdown
  50. {
  51. public:
  52. JuceMainMenuHandler()
  53. {
  54. static JuceMenuCallbackClass cls;
  55. callback = [cls.createInstance() init];
  56. JuceMenuCallbackClass::setOwner (callback, this);
  57. }
  58. ~JuceMainMenuHandler() override
  59. {
  60. setMenu (nullptr, nullptr, String());
  61. jassert (instance == this);
  62. instance = nullptr;
  63. [callback release];
  64. }
  65. void setMenu (MenuBarModel* const newMenuBarModel,
  66. const PopupMenu* newExtraAppleMenuItems,
  67. const String& recentItemsName)
  68. {
  69. recentItemsMenuName = recentItemsName;
  70. if (currentModel != newMenuBarModel)
  71. {
  72. if (currentModel != nullptr)
  73. currentModel->removeListener (this);
  74. currentModel = newMenuBarModel;
  75. if (currentModel != nullptr)
  76. currentModel->addListener (this);
  77. menuBarItemsChanged (nullptr);
  78. }
  79. extraAppleMenuItems.reset (createCopyIfNotNull (newExtraAppleMenuItems));
  80. }
  81. void addTopLevelMenu (NSMenu* parent, const PopupMenu& child, const String& name, int menuId, int topLevelIndex)
  82. {
  83. NSMenuItem* item = [parent addItemWithTitle: juceStringToNS (name)
  84. action: nil
  85. keyEquivalent: nsEmptyString()];
  86. NSMenu* sub = createMenu (child, name, menuId, topLevelIndex, true);
  87. [parent setSubmenu: sub forItem: item];
  88. [sub release];
  89. }
  90. void updateTopLevelMenu (NSMenuItem* parentItem, const PopupMenu& menuToCopy, const String& name, int menuId, int topLevelIndex)
  91. {
  92. // Note: This method used to update the contents of the existing menu in-place, but that caused
  93. // weird side-effects which messed-up keyboard focus when switching between windows. By creating
  94. // a new menu and replacing the old one with it, that problem seems to be avoided..
  95. NSMenu* menu = [[NSMenu alloc] initWithTitle: juceStringToNS (name)];
  96. for (PopupMenu::MenuItemIterator iter (menuToCopy); iter.next();)
  97. addMenuItem (iter, menu, menuId, topLevelIndex);
  98. [menu update];
  99. removeItemRecursive ([parentItem submenu]);
  100. [parentItem setSubmenu: menu];
  101. [menu release];
  102. }
  103. void updateTopLevelMenu (NSMenu* menu)
  104. {
  105. NSMenu* superMenu = [menu supermenu];
  106. auto menuNames = currentModel->getMenuBarNames();
  107. auto indexOfMenu = (int) [superMenu indexOfItemWithSubmenu: menu] - 1;
  108. if (indexOfMenu >= 0)
  109. {
  110. removeItemRecursive (menu);
  111. auto updatedPopup = currentModel->getMenuForIndex (indexOfMenu, menuNames[indexOfMenu]);
  112. for (PopupMenu::MenuItemIterator iter (updatedPopup); iter.next();)
  113. addMenuItem (iter, menu, 1, indexOfMenu);
  114. [menu update];
  115. }
  116. }
  117. void menuBarItemsChanged (MenuBarModel*) override
  118. {
  119. if (isOpen)
  120. {
  121. defferedUpdateRequested = true;
  122. return;
  123. }
  124. lastUpdateTime = Time::getMillisecondCounter();
  125. StringArray menuNames;
  126. if (currentModel != nullptr)
  127. menuNames = currentModel->getMenuBarNames();
  128. auto* menuBar = getMainMenuBar();
  129. while ([menuBar numberOfItems] > 1 + menuNames.size())
  130. removeItemRecursive (menuBar, static_cast<int> ([menuBar numberOfItems] - 1));
  131. int menuId = 1;
  132. for (int i = 0; i < menuNames.size(); ++i)
  133. {
  134. const PopupMenu menu (currentModel->getMenuForIndex (i, menuNames[i]));
  135. if (i >= [menuBar numberOfItems] - 1)
  136. addTopLevelMenu (menuBar, menu, menuNames[i], menuId, i);
  137. else
  138. updateTopLevelMenu ([menuBar itemAtIndex: 1 + i], menu, menuNames[i], menuId, i);
  139. }
  140. }
  141. void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info) override
  142. {
  143. if ((info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) == 0
  144. && info.invocationMethod != ApplicationCommandTarget::InvocationInfo::fromKeyPress)
  145. if (auto* item = findMenuItemWithCommandID (getMainMenuBar(), info.commandID))
  146. flashMenuBar ([item menu]);
  147. }
  148. void invoke (const PopupMenu::Item& item, int topLevelIndex) const
  149. {
  150. if (currentModel != nullptr)
  151. {
  152. if (item.action != nullptr)
  153. {
  154. MessageManager::callAsync (item.action);
  155. return;
  156. }
  157. if (item.customCallback != nullptr)
  158. if (! item.customCallback->menuItemTriggered())
  159. return;
  160. if (item.commandManager != nullptr)
  161. {
  162. ApplicationCommandTarget::InvocationInfo info (item.itemID);
  163. info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
  164. item.commandManager->invoke (info, true);
  165. }
  166. MessageManager::callAsync ([=]
  167. {
  168. if (instance != nullptr)
  169. instance->invokeDirectly (item.itemID, topLevelIndex);
  170. });
  171. }
  172. }
  173. void invokeDirectly (int commandId, int topLevelIndex)
  174. {
  175. if (currentModel != nullptr)
  176. currentModel->menuItemSelected (commandId, topLevelIndex);
  177. }
  178. void addMenuItem (PopupMenu::MenuItemIterator& iter, NSMenu* menuToAddTo,
  179. const int topLevelMenuId, const int topLevelIndex)
  180. {
  181. const PopupMenu::Item& i = iter.getItem();
  182. NSString* text = juceStringToNS (i.text);
  183. if (text == nil)
  184. text = nsEmptyString();
  185. if (i.isSeparator)
  186. {
  187. [menuToAddTo addItem: [NSMenuItem separatorItem]];
  188. }
  189. else if (i.isSectionHeader)
  190. {
  191. NSMenuItem* item = [menuToAddTo addItemWithTitle: text
  192. action: nil
  193. keyEquivalent: nsEmptyString()];
  194. [item setEnabled: false];
  195. }
  196. else if (i.subMenu != nullptr)
  197. {
  198. if (recentItemsMenuName.isNotEmpty() && i.text == recentItemsMenuName)
  199. {
  200. if (recent == nullptr)
  201. recent = std::make_unique<RecentFilesMenuItem>();
  202. if (recent->recentItem != nil)
  203. {
  204. if (NSMenu* parent = [recent->recentItem menu])
  205. [parent removeItem: recent->recentItem];
  206. [menuToAddTo addItem: recent->recentItem];
  207. return;
  208. }
  209. }
  210. NSMenuItem* item = [menuToAddTo addItemWithTitle: text
  211. action: nil
  212. keyEquivalent: nsEmptyString()];
  213. [item setTag: i.itemID];
  214. [item setEnabled: i.isEnabled];
  215. NSMenu* sub = createMenu (*i.subMenu, i.text, topLevelMenuId, topLevelIndex, false);
  216. [menuToAddTo setSubmenu: sub forItem: item];
  217. [sub release];
  218. }
  219. else
  220. {
  221. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
  222. auto item = [[NSMenuItem alloc] initWithTitle: text
  223. action: @selector (menuItemInvoked:)
  224. keyEquivalent: nsEmptyString()];
  225. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  226. [item setTag: topLevelIndex];
  227. [item setEnabled: i.isEnabled];
  228. #if defined (MAC_OS_X_VERSION_10_13) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_13
  229. [item setState: i.isTicked ? NSControlStateValueOn : NSControlStateValueOff];
  230. #else
  231. [item setState: i.isTicked ? NSOnState : NSOffState];
  232. #endif
  233. [item setTarget: (id) callback];
  234. auto* juceItem = new PopupMenu::Item (i);
  235. juceItem->customComponent = nullptr;
  236. [item setRepresentedObject: [createNSObjectFromJuceClass (juceItem) autorelease]];
  237. if (i.commandManager != nullptr)
  238. {
  239. for (auto& kp : i.commandManager->getKeyMappings()->getKeyPressesAssignedToCommand (i.itemID))
  240. {
  241. if (kp != KeyPress::backspaceKey // (adding these is annoying because it flashes the menu bar
  242. && kp != KeyPress::deleteKey) // every time you press the key while editing text)
  243. {
  244. juce_wchar key = kp.getTextCharacter();
  245. if (key == 0)
  246. key = (juce_wchar) kp.getKeyCode();
  247. [item setKeyEquivalent: juceStringToNS (String::charToString (key).toLowerCase())];
  248. [item setKeyEquivalentModifierMask: juceModsToNSMods (kp.getModifiers())];
  249. }
  250. break;
  251. }
  252. }
  253. [menuToAddTo addItem: item];
  254. [item release];
  255. }
  256. }
  257. NSMenu* createMenu (const PopupMenu menu,
  258. const String& menuName,
  259. const int topLevelMenuId,
  260. const int topLevelIndex,
  261. const bool addDelegate)
  262. {
  263. NSMenu* m = [[NSMenu alloc] initWithTitle: juceStringToNS (menuName)];
  264. if (addDelegate)
  265. [m setDelegate: (id<NSMenuDelegate>) callback];
  266. for (PopupMenu::MenuItemIterator iter (menu); iter.next();)
  267. addMenuItem (iter, m, topLevelMenuId, topLevelIndex);
  268. [m update];
  269. return m;
  270. }
  271. static JuceMainMenuHandler* instance;
  272. MenuBarModel* currentModel = nullptr;
  273. std::unique_ptr<PopupMenu> extraAppleMenuItems;
  274. uint32 lastUpdateTime = 0;
  275. NSObject* callback = nil;
  276. String recentItemsMenuName;
  277. bool isOpen = false, defferedUpdateRequested = false;
  278. private:
  279. struct RecentFilesMenuItem
  280. {
  281. RecentFilesMenuItem() : recentItem (nil)
  282. {
  283. if (NSNib* menuNib = [[[NSNib alloc] initWithNibNamed: @"RecentFilesMenuTemplate" bundle: nil] autorelease])
  284. {
  285. NSArray* array = nil;
  286. #if (! defined (MAC_OS_X_VERSION_10_8)) || MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_8
  287. [menuNib instantiateNibWithOwner: NSApp topLevelObjects: &array];
  288. #else
  289. [menuNib instantiateWithOwner: NSApp topLevelObjects: &array];
  290. #endif
  291. for (id object in array)
  292. {
  293. if ([object isKindOfClass: [NSMenu class]])
  294. {
  295. if (NSArray* items = [object itemArray])
  296. {
  297. if (NSMenuItem* item = findRecentFilesItem (items))
  298. {
  299. recentItem = [item retain];
  300. break;
  301. }
  302. }
  303. }
  304. }
  305. }
  306. }
  307. ~RecentFilesMenuItem()
  308. {
  309. [recentItem release];
  310. }
  311. static NSMenuItem* findRecentFilesItem (NSArray* const items)
  312. {
  313. for (id object in items)
  314. if (NSArray* subMenuItems = [[object submenu] itemArray])
  315. for (id subObject in subMenuItems)
  316. if ([subObject isKindOfClass: [NSMenuItem class]])
  317. return subObject;
  318. return nil;
  319. }
  320. NSMenuItem* recentItem;
  321. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RecentFilesMenuItem)
  322. };
  323. std::unique_ptr<RecentFilesMenuItem> recent;
  324. //==============================================================================
  325. static NSMenuItem* findMenuItemWithCommandID (NSMenu* const menu, int commandID)
  326. {
  327. for (NSInteger i = [menu numberOfItems]; --i >= 0;)
  328. {
  329. NSMenuItem* m = [menu itemAtIndex: i];
  330. if (auto* menuItem = getJuceClassFromNSObject<PopupMenu::Item> ([m representedObject]))
  331. if (menuItem->itemID == commandID)
  332. return m;
  333. if (NSMenu* sub = [m submenu])
  334. if (NSMenuItem* found = findMenuItemWithCommandID (sub, commandID))
  335. return found;
  336. }
  337. return nil;
  338. }
  339. static void flashMenuBar (NSMenu* menu)
  340. {
  341. if ([[menu title] isEqualToString: nsStringLiteral ("Apple")])
  342. return;
  343. [menu retain];
  344. const unichar f35Key = NSF35FunctionKey;
  345. NSString* f35String = [NSString stringWithCharacters: &f35Key length: 1];
  346. NSMenuItem* item = [[NSMenuItem alloc] initWithTitle: nsStringLiteral ("x")
  347. action: nil
  348. keyEquivalent: f35String];
  349. [item setTarget: nil];
  350. [menu insertItem: item atIndex: [menu numberOfItems]];
  351. [item release];
  352. if ([menu indexOfItem: item] >= 0)
  353. {
  354. NSEvent* f35Event = [NSEvent keyEventWithType: NSEventTypeKeyDown
  355. location: NSZeroPoint
  356. modifierFlags: NSEventModifierFlagCommand
  357. timestamp: 0
  358. windowNumber: 0
  359. context: [NSGraphicsContext currentContext]
  360. characters: f35String
  361. charactersIgnoringModifiers: f35String
  362. isARepeat: NO
  363. keyCode: 0];
  364. [menu performKeyEquivalent: f35Event];
  365. if ([menu indexOfItem: item] >= 0)
  366. [menu removeItem: item]; // (this throws if the item isn't actually in the menu)
  367. }
  368. [menu release];
  369. }
  370. static unsigned int juceModsToNSMods (const ModifierKeys mods)
  371. {
  372. unsigned int m = 0;
  373. if (mods.isShiftDown()) m |= NSEventModifierFlagShift;
  374. if (mods.isCtrlDown()) m |= NSEventModifierFlagControl;
  375. if (mods.isAltDown()) m |= NSEventModifierFlagOption;
  376. if (mods.isCommandDown()) m |= NSEventModifierFlagCommand;
  377. return m;
  378. }
  379. // Apple Bug: For some reason [NSMenu removeAllItems] seems to leak it's objects
  380. // on shutdown, so we need this method to release the items one-by-one manually
  381. static void removeItemRecursive (NSMenu* parentMenu, int menuItemIndex)
  382. {
  383. if (isPositiveAndBelow (menuItemIndex, (int) [parentMenu numberOfItems]))
  384. {
  385. auto menuItem = [parentMenu itemAtIndex:menuItemIndex];
  386. if (auto submenu = [menuItem submenu])
  387. removeItemRecursive (submenu);
  388. [parentMenu removeItem:menuItem];
  389. }
  390. else
  391. jassertfalse;
  392. }
  393. static void removeItemRecursive (NSMenu* menu)
  394. {
  395. if (menu != nullptr)
  396. {
  397. auto n = static_cast<int> ([menu numberOfItems]);
  398. for (auto i = n; --i >= 0;)
  399. removeItemRecursive (menu, i);
  400. }
  401. }
  402. static NSMenu* getMainMenuBar()
  403. {
  404. return JuceMainMenuBarHolder::getInstance()->mainMenuBar;
  405. }
  406. //==============================================================================
  407. struct JuceMenuCallbackClass : public ObjCClass<NSObject>
  408. {
  409. JuceMenuCallbackClass() : ObjCClass ("JUCEMainMenu_")
  410. {
  411. addIvar<JuceMainMenuHandler*> ("owner");
  412. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
  413. addMethod (@selector (menuItemInvoked:), menuItemInvoked, "v@:@");
  414. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  415. addMethod (@selector (menuNeedsUpdate:), menuNeedsUpdate, "v@:@");
  416. addMethod (@selector (validateMenuItem:), validateMenuItem, "c@:@");
  417. addProtocol (@protocol (NSMenuDelegate));
  418. #if defined (MAC_OS_X_VERSION_10_14)
  419. addProtocol (@protocol (NSMenuItemValidation));
  420. #endif
  421. registerClass();
  422. }
  423. static void setOwner (id self, JuceMainMenuHandler* owner)
  424. {
  425. object_setInstanceVariable (self, "owner", owner);
  426. }
  427. private:
  428. static auto* getPopupMenuItem (NSMenuItem* item)
  429. {
  430. return getJuceClassFromNSObject<PopupMenu::Item> ([item representedObject]);
  431. }
  432. static auto* getOwner (id self)
  433. {
  434. return getIvar<JuceMainMenuHandler*> (self, "owner");
  435. }
  436. static void menuItemInvoked (id self, SEL, NSMenuItem* item)
  437. {
  438. if (auto* juceItem = getPopupMenuItem (item))
  439. getOwner (self)->invoke (*juceItem, static_cast<int> ([item tag]));
  440. }
  441. static void menuNeedsUpdate (id self, SEL, NSMenu* menu)
  442. {
  443. getOwner (self)->updateTopLevelMenu (menu);
  444. }
  445. static BOOL validateMenuItem (id, SEL, NSMenuItem* item)
  446. {
  447. if (auto* juceItem = getPopupMenuItem (item))
  448. return juceItem->isEnabled;
  449. return YES;
  450. }
  451. };
  452. };
  453. JuceMainMenuHandler* JuceMainMenuHandler::instance = nullptr;
  454. //==============================================================================
  455. class TemporaryMainMenuWithStandardCommands
  456. {
  457. public:
  458. explicit TemporaryMainMenuWithStandardCommands (FilePreviewComponent* filePreviewComponent)
  459. : oldMenu (MenuBarModel::getMacMainMenu()), dummyModalComponent (filePreviewComponent)
  460. {
  461. if (auto* appleMenu = MenuBarModel::getMacExtraAppleItemsMenu())
  462. oldAppleMenu = std::make_unique<PopupMenu> (*appleMenu);
  463. if (auto* handler = JuceMainMenuHandler::instance)
  464. oldRecentItems = handler->recentItemsMenuName;
  465. MenuBarModel::setMacMainMenu (nullptr);
  466. if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
  467. {
  468. NSMenu* menu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Edit")];
  469. NSMenuItem* item;
  470. item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Cut"), nil)
  471. action: @selector (cut:) keyEquivalent: nsStringLiteral ("x")];
  472. [menu addItem: item];
  473. [item release];
  474. item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Copy"), nil)
  475. action: @selector (copy:) keyEquivalent: nsStringLiteral ("c")];
  476. [menu addItem: item];
  477. [item release];
  478. item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Paste"), nil)
  479. action: @selector (paste:) keyEquivalent: nsStringLiteral ("v")];
  480. [menu addItem: item];
  481. [item release];
  482. editMenuIndex = [mainMenu numberOfItems];
  483. item = [mainMenu addItemWithTitle: NSLocalizedString (nsStringLiteral ("Edit"), nil)
  484. action: nil keyEquivalent: nsEmptyString()];
  485. [mainMenu setSubmenu: menu forItem: item];
  486. [menu release];
  487. }
  488. // use a dummy modal component so that apps can tell that something is currently modal.
  489. dummyModalComponent.enterModalState (false);
  490. }
  491. ~TemporaryMainMenuWithStandardCommands()
  492. {
  493. if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
  494. [mainMenu removeItemAtIndex:editMenuIndex];
  495. MenuBarModel::setMacMainMenu (oldMenu, oldAppleMenu.get(), oldRecentItems);
  496. }
  497. static bool checkModalEvent (FilePreviewComponent* preview, const Component* targetComponent)
  498. {
  499. if (targetComponent == nullptr)
  500. return false;
  501. return (targetComponent == preview
  502. || targetComponent->findParentComponentOfClass<FilePreviewComponent>() != nullptr);
  503. }
  504. private:
  505. MenuBarModel* const oldMenu = nullptr;
  506. std::unique_ptr<PopupMenu> oldAppleMenu;
  507. String oldRecentItems;
  508. NSInteger editMenuIndex;
  509. // The OS view already plays an alert when clicking outside
  510. // the modal comp, so this override avoids adding extra
  511. // inappropriate noises when the cancel button is pressed.
  512. // This override is also important because it stops the base class
  513. // calling ModalComponentManager::bringToFront, which can get
  514. // recursive when file dialogs are involved
  515. struct SilentDummyModalComp : public Component
  516. {
  517. explicit SilentDummyModalComp (FilePreviewComponent* p)
  518. : preview (p) {}
  519. void inputAttemptWhenModal() override {}
  520. bool canModalEventBeSentToComponent (const Component* targetComponent) override
  521. {
  522. return checkModalEvent (preview, targetComponent);
  523. }
  524. FilePreviewComponent* preview = nullptr;
  525. };
  526. SilentDummyModalComp dummyModalComponent;
  527. };
  528. //==============================================================================
  529. namespace MainMenuHelpers
  530. {
  531. static NSString* translateMenuName (const String& name)
  532. {
  533. return NSLocalizedString (juceStringToNS (TRANS (name)), nil);
  534. }
  535. static NSMenuItem* createMenuItem (NSMenu* menu, const String& name, SEL sel, NSString* key)
  536. {
  537. NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle: translateMenuName (name)
  538. action: sel
  539. keyEquivalent: key] autorelease];
  540. [item setTarget: NSApp];
  541. [menu addItem: item];
  542. return item;
  543. }
  544. static void createStandardAppMenu (NSMenu* menu, const String& appName, const PopupMenu* extraItems)
  545. {
  546. if (extraItems != nullptr && JuceMainMenuHandler::instance != nullptr && extraItems->getNumItems() > 0)
  547. {
  548. for (PopupMenu::MenuItemIterator iter (*extraItems); iter.next();)
  549. JuceMainMenuHandler::instance->addMenuItem (iter, menu, 0, -1);
  550. [menu addItem: [NSMenuItem separatorItem]];
  551. }
  552. // Services...
  553. NSMenuItem* services = [[[NSMenuItem alloc] initWithTitle: translateMenuName ("Services")
  554. action: nil keyEquivalent: nsEmptyString()] autorelease];
  555. [menu addItem: services];
  556. NSMenu* servicesMenu = [[[NSMenu alloc] initWithTitle: translateMenuName ("Services")] autorelease];
  557. [menu setSubmenu: servicesMenu forItem: services];
  558. [NSApp setServicesMenu: servicesMenu];
  559. [menu addItem: [NSMenuItem separatorItem]];
  560. createMenuItem (menu, TRANS("Hide") + String (" ") + appName, @selector (hide:), nsStringLiteral ("h"));
  561. [createMenuItem (menu, TRANS("Hide Others"), @selector (hideOtherApplications:), nsStringLiteral ("h"))
  562. setKeyEquivalentModifierMask: NSEventModifierFlagCommand | NSEventModifierFlagOption];
  563. createMenuItem (menu, TRANS("Show All"), @selector (unhideAllApplications:), nsEmptyString());
  564. [menu addItem: [NSMenuItem separatorItem]];
  565. createMenuItem (menu, TRANS("Quit") + String (" ") + appName, @selector (terminate:), nsStringLiteral ("q"));
  566. }
  567. // Since our app has no NIB, this initialises a standard app menu...
  568. static void rebuildMainMenu (const PopupMenu* extraItems)
  569. {
  570. // this can't be used in a plugin!
  571. jassert (JUCEApplicationBase::isStandaloneApp());
  572. if (auto* app = JUCEApplicationBase::getInstance())
  573. {
  574. if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
  575. {
  576. if ([mainMenu numberOfItems] > 0)
  577. {
  578. if (auto appMenu = [[mainMenu itemAtIndex: 0] submenu])
  579. {
  580. [appMenu removeAllItems];
  581. MainMenuHelpers::createStandardAppMenu (appMenu, app->getApplicationName(), extraItems);
  582. }
  583. }
  584. }
  585. }
  586. }
  587. }
  588. void MenuBarModel::setMacMainMenu (MenuBarModel* newMenuBarModel,
  589. const PopupMenu* extraAppleMenuItems,
  590. const String& recentItemsMenuName)
  591. {
  592. if (getMacMainMenu() != newMenuBarModel)
  593. {
  594. JUCE_AUTORELEASEPOOL
  595. {
  596. if (newMenuBarModel == nullptr)
  597. {
  598. delete JuceMainMenuHandler::instance;
  599. jassert (JuceMainMenuHandler::instance == nullptr); // should be zeroed in the destructor
  600. jassert (extraAppleMenuItems == nullptr); // you can't specify some extra items without also supplying a model
  601. extraAppleMenuItems = nullptr;
  602. }
  603. else
  604. {
  605. if (JuceMainMenuHandler::instance == nullptr)
  606. JuceMainMenuHandler::instance = new JuceMainMenuHandler();
  607. JuceMainMenuHandler::instance->setMenu (newMenuBarModel, extraAppleMenuItems, recentItemsMenuName);
  608. }
  609. }
  610. }
  611. MainMenuHelpers::rebuildMainMenu (extraAppleMenuItems);
  612. if (newMenuBarModel != nullptr)
  613. newMenuBarModel->menuItemsChanged();
  614. }
  615. MenuBarModel* MenuBarModel::getMacMainMenu()
  616. {
  617. if (auto* mm = JuceMainMenuHandler::instance)
  618. return mm->currentModel;
  619. return nullptr;
  620. }
  621. const PopupMenu* MenuBarModel::getMacExtraAppleItemsMenu()
  622. {
  623. if (auto* mm = JuceMainMenuHandler::instance)
  624. return mm->extraAppleMenuItems.get();
  625. return nullptr;
  626. }
  627. using MenuTrackingChangedCallback = void (*)(bool);
  628. extern MenuTrackingChangedCallback menuTrackingChangedCallback;
  629. static void mainMenuTrackingChanged (bool isTracking)
  630. {
  631. PopupMenu::dismissAllActiveMenus();
  632. if (auto* menuHandler = JuceMainMenuHandler::instance)
  633. {
  634. menuHandler->isOpen = isTracking;
  635. if (auto* model = menuHandler->currentModel)
  636. model->handleMenuBarActivate (isTracking);
  637. if (menuHandler->defferedUpdateRequested && ! isTracking)
  638. {
  639. menuHandler->defferedUpdateRequested = false;
  640. menuHandler->menuBarItemsChanged (menuHandler->currentModel);
  641. }
  642. }
  643. }
  644. void juce_initialiseMacMainMenu()
  645. {
  646. menuTrackingChangedCallback = mainMenuTrackingChanged;
  647. if (JuceMainMenuHandler::instance == nullptr)
  648. MainMenuHelpers::rebuildMainMenu (nullptr);
  649. }
  650. // (used from other modules that need to create an NSMenu)
  651. NSMenu* createNSMenu (const PopupMenu&, const String&, int, int, bool);
  652. NSMenu* createNSMenu (const PopupMenu& menu, const String& name, int topLevelMenuId, int topLevelIndex, bool addDelegate)
  653. {
  654. juce_initialiseMacMainMenu();
  655. if (auto* mm = JuceMainMenuHandler::instance)
  656. return mm->createMenu (menu, name, topLevelMenuId, topLevelIndex, addDelegate);
  657. jassertfalse; // calling this before making sure the OSX main menu stuff was initialised?
  658. return nil;
  659. }
  660. } // namespace juce