atom_menu_controller.mm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. // Copyright (c) 2013 GitHub, Inc.
  2. // Copyright (c) 2012 The Chromium Authors. All rights reserved.
  3. // Use of this source code is governed by the MIT license that can be
  4. // found in the LICENSE file.
  5. #import "atom/browser/ui/cocoa/atom_menu_controller.h"
  6. #include "atom/browser/ui/atom_menu_model.h"
  7. #include "base/logging.h"
  8. #include "base/strings/sys_string_conversions.h"
  9. #include "base/strings/utf_string_conversions.h"
  10. #include "content/public/browser/browser_thread.h"
  11. #include "ui/base/accelerators/accelerator.h"
  12. #include "ui/base/accelerators/platform_accelerator_cocoa.h"
  13. #include "ui/base/l10n/l10n_util_mac.h"
  14. #include "ui/events/cocoa/cocoa_event_utils.h"
  15. #include "ui/gfx/image/image.h"
  16. using content::BrowserThread;
  17. namespace {
  18. struct Role {
  19. SEL selector;
  20. const char* role;
  21. };
  22. Role kRolesMap[] = {
  23. {@selector(orderFrontStandardAboutPanel:), "about"},
  24. {@selector(hide:), "hide"},
  25. {@selector(hideOtherApplications:), "hideothers"},
  26. {@selector(unhideAllApplications:), "unhide"},
  27. {@selector(arrangeInFront:), "front"},
  28. {@selector(undo:), "undo"},
  29. {@selector(redo:), "redo"},
  30. {@selector(cut:), "cut"},
  31. {@selector(copy:), "copy"},
  32. {@selector(paste:), "paste"},
  33. {@selector(delete:), "delete"},
  34. {@selector(pasteAndMatchStyle:), "pasteandmatchstyle"},
  35. {@selector(selectAll:), "selectall"},
  36. {@selector(startSpeaking:), "startspeaking"},
  37. {@selector(stopSpeaking:), "stopspeaking"},
  38. {@selector(performMiniaturize:), "minimize"},
  39. {@selector(performClose:), "close"},
  40. {@selector(performZoom:), "zoom"},
  41. {@selector(terminate:), "quit"},
  42. // ↓ is intentionally not `toggleFullScreen`. The macOS full screen menu
  43. // item behaves weird. If we use `toggleFullScreen`, then the menu item will
  44. // use the default label, and not take the one provided.
  45. {@selector(toggleFullScreenMode:), "togglefullscreen"},
  46. {@selector(toggleTabBar:), "toggletabbar"},
  47. {@selector(selectNextTab:), "selectnexttab"},
  48. {@selector(selectPreviousTab:), "selectprevioustab"},
  49. {@selector(mergeAllWindows:), "mergeallwindows"},
  50. {@selector(moveTabToNewWindow:), "movetabtonewwindow"},
  51. {@selector(clearRecentDocuments:), "clearrecentdocuments"},
  52. };
  53. } // namespace
  54. // Menu item is located for ease of removing it from the parent owner
  55. static base::scoped_nsobject<NSMenuItem> recentDocumentsMenuItem_;
  56. // Submenu retained to be swapped back to |recentDocumentsMenuItem_|
  57. static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
  58. @implementation AtomMenuController
  59. @synthesize model = model_;
  60. - (id)initWithModel:(atom::AtomMenuModel*)model
  61. useDefaultAccelerator:(BOOL)use {
  62. if ((self = [super init])) {
  63. model_ = model;
  64. isMenuOpen_ = NO;
  65. useDefaultAccelerator_ = use;
  66. [self menu];
  67. }
  68. return self;
  69. }
  70. - (void)dealloc {
  71. [menu_ setDelegate:nil];
  72. // Close the menu if it is still open. This could happen if a tab gets closed
  73. // while its context menu is still open.
  74. [self cancel];
  75. model_ = nil;
  76. [super dealloc];
  77. }
  78. - (void)setCloseCallback:(const base::Callback<void()>&)callback {
  79. closeCallback = callback;
  80. }
  81. - (void)populateWithModel:(atom::AtomMenuModel*)model {
  82. if (!menu_)
  83. return;
  84. if (!recentDocumentsMenuItem_) {
  85. // Locate & retain the recent documents menu item
  86. recentDocumentsMenuItem_.reset(
  87. [[[[[NSApp mainMenu] itemWithTitle:@"Electron"] submenu]
  88. itemWithTitle:@"Open Recent"] retain]);
  89. }
  90. model_ = model;
  91. [menu_ removeAllItems];
  92. const int count = model->GetItemCount();
  93. for (int index = 0; index < count; index++) {
  94. if (model->GetTypeAt(index) == atom::AtomMenuModel::TYPE_SEPARATOR)
  95. [self addSeparatorToMenu:menu_ atIndex:index];
  96. else
  97. [self addItemToMenu:menu_ atIndex:index fromModel:model];
  98. }
  99. }
  100. - (void)cancel {
  101. if (isMenuOpen_) {
  102. [menu_ cancelTracking];
  103. isMenuOpen_ = NO;
  104. model_->MenuWillClose();
  105. closeCallback.Run();
  106. }
  107. }
  108. // Creates a NSMenu from the given model. If the model has submenus, this can
  109. // be invoked recursively.
  110. - (NSMenu*)menuFromModel:(atom::AtomMenuModel*)model {
  111. NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease];
  112. const int count = model->GetItemCount();
  113. for (int index = 0; index < count; index++) {
  114. if (model->GetTypeAt(index) == atom::AtomMenuModel::TYPE_SEPARATOR)
  115. [self addSeparatorToMenu:menu atIndex:index];
  116. else
  117. [self addItemToMenu:menu atIndex:index fromModel:model];
  118. }
  119. return menu;
  120. }
  121. // Adds a separator item at the given index. As the separator doesn't need
  122. // anything from the model, this method doesn't need the model index as the
  123. // other method below does.
  124. - (void)addSeparatorToMenu:(NSMenu*)menu atIndex:(int)index {
  125. NSMenuItem* separator = [NSMenuItem separatorItem];
  126. [menu insertItem:separator atIndex:index];
  127. }
  128. // Empties the source menu items to the destination.
  129. - (void)moveMenuItems:(NSMenu*)source to:(NSMenu*)destination {
  130. const long count = [source numberOfItems];
  131. for (long index = 0; index < count; index++) {
  132. NSMenuItem* removedItem = [[[source itemAtIndex:0] retain] autorelease];
  133. [source removeItemAtIndex:0];
  134. [destination addItem:removedItem];
  135. }
  136. }
  137. // Replaces the item's submenu instance with the singleton recent documents
  138. // menu. Previously replaced menu items will be recovered.
  139. - (void)replaceSubmenuShowingRecentDocuments:(NSMenuItem*)item {
  140. NSMenu* recentDocumentsMenu =
  141. [[[recentDocumentsMenuItem_ submenu] retain] autorelease];
  142. // Remove menu items in recent documents back to swap menu
  143. [self moveMenuItems:recentDocumentsMenu to:recentDocumentsMenuSwap_];
  144. // Swap back the submenu
  145. [recentDocumentsMenuItem_ setSubmenu:recentDocumentsMenuSwap_];
  146. // Retain the item's submenu for a future recovery
  147. recentDocumentsMenuSwap_.reset([[item submenu] retain]);
  148. // Repopulate with items from the submenu to be replaced
  149. [self moveMenuItems:recentDocumentsMenuSwap_ to:recentDocumentsMenu];
  150. // Update the submenu's title
  151. [recentDocumentsMenu setTitle:[recentDocumentsMenuSwap_ title]];
  152. // Replace submenu
  153. [item setSubmenu:recentDocumentsMenu];
  154. // Remember the new menu item that carries the recent documents menu
  155. recentDocumentsMenuItem_.reset([item retain]);
  156. }
  157. // Adds an item or a hierarchical menu to the item at the |index|,
  158. // associated with the entry in the model identified by |modelIndex|.
  159. - (void)addItemToMenu:(NSMenu*)menu
  160. atIndex:(NSInteger)index
  161. fromModel:(atom::AtomMenuModel*)model {
  162. base::string16 label16 = model->GetLabelAt(index);
  163. NSString* label = l10n_util::FixUpWindowsStyleLabel(label16);
  164. base::scoped_nsobject<NSMenuItem> item([[NSMenuItem alloc]
  165. initWithTitle:label
  166. action:@selector(itemSelected:)
  167. keyEquivalent:@""]);
  168. // If the menu item has an icon, set it.
  169. gfx::Image icon;
  170. if (model->GetIconAt(index, &icon) && !icon.IsEmpty())
  171. [item setImage:icon.ToNSImage()];
  172. base::string16 role = model->GetRoleAt(index);
  173. atom::AtomMenuModel::ItemType type = model->GetTypeAt(index);
  174. if (type == atom::AtomMenuModel::TYPE_SUBMENU) {
  175. // Recursively build a submenu from the sub-model at this index.
  176. [item setTarget:nil];
  177. [item setAction:nil];
  178. atom::AtomMenuModel* submenuModel =
  179. static_cast<atom::AtomMenuModel*>(model->GetSubmenuModelAt(index));
  180. NSMenu* submenu = [self menuFromModel:submenuModel];
  181. [submenu setTitle:[item title]];
  182. [item setSubmenu:submenu];
  183. // Set submenu's role.
  184. if (role == base::ASCIIToUTF16("window") && [submenu numberOfItems])
  185. [NSApp setWindowsMenu:submenu];
  186. else if (role == base::ASCIIToUTF16("help"))
  187. [NSApp setHelpMenu:submenu];
  188. else if (role == base::ASCIIToUTF16("services"))
  189. [NSApp setServicesMenu:submenu];
  190. else if (role == base::ASCIIToUTF16("recentdocuments"))
  191. [self replaceSubmenuShowingRecentDocuments:item];
  192. } else {
  193. // The MenuModel works on indexes so we can't just set the command id as the
  194. // tag like we do in other menus. Also set the represented object to be
  195. // the model so hierarchical menus check the correct index in the correct
  196. // model. Setting the target to |self| allows this class to participate
  197. // in validation of the menu items.
  198. [item setTag:index];
  199. NSValue* modelObject = [NSValue valueWithPointer:model];
  200. [item setRepresentedObject:modelObject]; // Retains |modelObject|.
  201. ui::Accelerator accelerator;
  202. if (model->GetAcceleratorAtWithParams(index, useDefaultAccelerator_,
  203. &accelerator)) {
  204. const ui::PlatformAcceleratorCocoa* platformAccelerator =
  205. static_cast<const ui::PlatformAcceleratorCocoa*>(
  206. accelerator.platform_accelerator());
  207. if (platformAccelerator) {
  208. [item setKeyEquivalent:platformAccelerator->characters()];
  209. [item
  210. setKeyEquivalentModifierMask:platformAccelerator->modifier_mask()];
  211. }
  212. }
  213. // Set menu item's role.
  214. [item setTarget:self];
  215. if (!role.empty()) {
  216. for (const Role& pair : kRolesMap) {
  217. if (role == base::ASCIIToUTF16(pair.role)) {
  218. [item setTarget:nil];
  219. [item setAction:pair.selector];
  220. break;
  221. }
  222. }
  223. }
  224. }
  225. [menu insertItem:item atIndex:index];
  226. }
  227. // Called before the menu is to be displayed to update the state (enabled,
  228. // radio, etc) of each item in the menu. Also will update the title if
  229. // the item is marked as "dynamic".
  230. - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  231. SEL action = [item action];
  232. if (action != @selector(itemSelected:))
  233. return NO;
  234. NSInteger modelIndex = [item tag];
  235. atom::AtomMenuModel* model = static_cast<atom::AtomMenuModel*>(
  236. [[(id)item representedObject] pointerValue]);
  237. DCHECK(model);
  238. if (model) {
  239. BOOL checked = model->IsItemCheckedAt(modelIndex);
  240. DCHECK([(id)item isKindOfClass:[NSMenuItem class]]);
  241. [(id)item setState:(checked ? NSOnState : NSOffState)];
  242. [(id)item setHidden:(!model->IsVisibleAt(modelIndex))];
  243. if (model->IsItemDynamicAt(modelIndex)) {
  244. // Update the label and the icon.
  245. NSString* label =
  246. l10n_util::FixUpWindowsStyleLabel(model->GetLabelAt(modelIndex));
  247. [(id)item setTitle:label];
  248. gfx::Image icon;
  249. model->GetIconAt(modelIndex, &icon);
  250. [(id)item setImage:icon.IsEmpty() ? nil : icon.ToNSImage()];
  251. }
  252. return model->IsEnabledAt(modelIndex);
  253. }
  254. return NO;
  255. }
  256. // Called when the user chooses a particular menu item. |sender| is the menu
  257. // item chosen.
  258. - (void)itemSelected:(id)sender {
  259. NSInteger modelIndex = [sender tag];
  260. atom::AtomMenuModel* model = static_cast<atom::AtomMenuModel*>(
  261. [[sender representedObject] pointerValue]);
  262. DCHECK(model);
  263. if (model) {
  264. NSEvent* event = [NSApp currentEvent];
  265. model->ActivatedAt(modelIndex,
  266. ui::EventFlagsFromModifiers([event modifierFlags]));
  267. }
  268. }
  269. - (NSMenu*)menu {
  270. if (menu_)
  271. return menu_.get();
  272. menu_.reset([[NSMenu alloc] initWithTitle:@""]);
  273. [menu_ setDelegate:self];
  274. if (model_)
  275. [self populateWithModel:model_];
  276. return menu_.get();
  277. }
  278. - (BOOL)isMenuOpen {
  279. return isMenuOpen_;
  280. }
  281. - (void)menuWillOpen:(NSMenu*)menu {
  282. isMenuOpen_ = YES;
  283. model_->MenuWillShow();
  284. }
  285. - (void)menuDidClose:(NSMenu*)menu {
  286. if (isMenuOpen_) {
  287. isMenuOpen_ = NO;
  288. model_->MenuWillClose();
  289. // Post async task so that itemSelected runs before the close callback
  290. // deletes the controller from the map which deallocates it
  291. if (!closeCallback.is_null()) {
  292. BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, closeCallback);
  293. }
  294. }
  295. }
  296. @end