1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/apps/app_shim_menu_controller_mac.h"
7 #include "base/mac/scoped_nsautorelease_pool.h"
8 #include "base/strings/sys_string_conversions.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "chrome/app/chrome_command_ids.h"
11 #include "chrome/browser/apps/app_shim/extension_app_shim_handler_mac.h"
12 #include "chrome/browser/apps/app_window_registry_util.h"
13 #import "chrome/browser/ui/cocoa/apps/native_app_window_cocoa.h"
14 #include "chrome/grit/generated_resources.h"
15 #include "extensions/browser/app_window/app_window.h"
16 #include "extensions/common/extension.h"
17 #include "ui/base/l10n/l10n_util.h"
18 #include "ui/base/l10n/l10n_util_mac.h"
22 // Gets an item from the main menu given the tag of the top level item
23 // |menu_tag| and the tag of the item |item_tag|.
24 NSMenuItem* GetItemByTag(NSInteger menu_tag, NSInteger item_tag) {
25 return [[[[NSApp mainMenu] itemWithTag:menu_tag] submenu]
26 itemWithTag:item_tag];
29 // Finds a top level menu item using |menu_tag| and creates a new NSMenuItem
30 // with the same title.
31 NSMenuItem* NewTopLevelItemFrom(NSInteger menu_tag) {
32 NSMenuItem* original = [[NSApp mainMenu] itemWithTag:menu_tag];
33 base::scoped_nsobject<NSMenuItem> item([[NSMenuItem alloc]
34 initWithTitle:[original title]
37 DCHECK([original hasSubmenu]);
38 base::scoped_nsobject<NSMenu> sub_menu([[NSMenu alloc]
39 initWithTitle:[[original submenu] title]]);
40 [item setSubmenu:sub_menu];
41 return item.autorelease();
44 // Finds an item using |menu_tag| and |item_tag| and adds a duplicate of it to
45 // the submenu of |top_level_item|.
46 void AddDuplicateItem(NSMenuItem* top_level_item,
49 base::scoped_nsobject<NSMenuItem> item(
50 [GetItemByTag(menu_tag, item_tag) copy]);
52 [[top_level_item submenu] addItem:item];
57 // Used by AppShimMenuController to manage menu items that are a copy of a
58 // Chrome menu item but with a different action. This manages unsetting and
59 // restoring the original item's key equivalent, so that we can use the same
60 // key equivalent in the copied item with a different action. If |resourceId_|
61 // is non-zero, this will also update the title to include the app name.
62 // If the copy (menuItem) has no key equivalent, and the title does not have the
63 // app name, then enableForApp and disable do not need to be called. I.e. the
64 // doppelganger just copies the item and sets a new action.
65 @interface DoppelgangerMenuItem : NSObject {
67 base::scoped_nsobject<NSMenuItem> menuItem_;
68 base::scoped_nsobject<NSMenuItem> sourceItem_;
69 base::scoped_nsobject<NSString> sourceKeyEquivalent_;
73 @property(readonly, nonatomic) NSMenuItem* menuItem;
75 // Get the source item using the tags and create the menu item.
76 - (id)initWithController:(AppShimMenuController*)controller
77 menuTag:(NSInteger)menuTag
78 itemTag:(NSInteger)itemTag
79 resourceId:(int)resourceId
81 keyEquivalent:(NSString*)keyEquivalent;
82 // Set the title using |resourceId_| and unset the source item's key equivalent.
83 - (void)enableForApp:(const extensions::Extension*)app;
84 // Restore the source item's key equivalent.
88 @implementation DoppelgangerMenuItem
90 - (NSMenuItem*)menuItem {
94 - (id)initWithController:(AppShimMenuController*)controller
95 menuTag:(NSInteger)menuTag
96 itemTag:(NSInteger)itemTag
97 resourceId:(int)resourceId
99 keyEquivalent:(NSString*)keyEquivalent {
100 if ((self = [super init])) {
101 sourceItem_.reset([GetItemByTag(menuTag, itemTag) retain]);
103 sourceKeyEquivalent_.reset([[sourceItem_ keyEquivalent] copy]);
104 menuItem_.reset([[NSMenuItem alloc]
105 initWithTitle:[sourceItem_ title]
107 keyEquivalent:keyEquivalent]);
108 [menuItem_ setTarget:controller];
109 [menuItem_ setTag:itemTag];
110 resourceId_ = resourceId;
115 - (void)enableForApp:(const extensions::Extension*)app {
116 // It seems that two menu items that have the same key equivalent must also
117 // have the same action for the keyboard shortcut to work. (This refers to the
118 // original keyboard shortcut, regardless of any overrides set in OSX).
119 // In order to let the app menu items have a different action, we remove the
120 // key equivalent of the original items and restore them later.
121 [sourceItem_ setKeyEquivalent:@""];
125 [menuItem_ setTitle:l10n_util::GetNSStringF(resourceId_,
126 base::UTF8ToUTF16(app->name()))];
130 // Restore the keyboard shortcut to Chrome. This just needs to be set back to
131 // the original keyboard shortcut, regardless of any overrides in OSX. The
132 // overrides still work as they are based on the title of the menu item.
133 [sourceItem_ setKeyEquivalent:sourceKeyEquivalent_];
138 @interface AppShimMenuController ()
139 // Construct the NSMenuItems for apps.
140 - (void)buildAppMenuItems;
141 // Register for NSWindow notifications.
142 - (void)registerEventHandlers;
143 // If the window is an app window, add or remove menu items.
144 - (void)windowMainStatusChanged:(NSNotification*)notification;
145 // Add menu items for an app and hide Chrome menu items.
146 - (void)addMenuItems:(const extensions::Extension*)app;
147 // If the window belongs to the currently focused app, remove the menu items and
148 // unhide Chrome menu items.
149 - (void)removeMenuItems;
150 // If the currently focused window belongs to a platform app, quit the app.
151 - (void)quitCurrentPlatformApp;
152 // If the currently focused window belongs to a platform app, hide the app.
153 - (void)hideCurrentPlatformApp;
154 // If the currently focused window belongs to a platform app, focus the app.
155 - (void)focusCurrentPlatformApp;
158 @implementation AppShimMenuController
161 if ((self = [super init])) {
162 [self buildAppMenuItems];
163 [self registerEventHandlers];
169 [[NSNotificationCenter defaultCenter] removeObserver:self];
173 - (void)buildAppMenuItems {
174 aboutDoppelganger_.reset([[DoppelgangerMenuItem alloc]
175 initWithController:self
176 menuTag:IDC_CHROME_MENU
178 resourceId:IDS_ABOUT_MAC
181 hideDoppelganger_.reset([[DoppelgangerMenuItem alloc]
182 initWithController:self
183 menuTag:IDC_CHROME_MENU
185 resourceId:IDS_HIDE_APP_MAC
186 action:@selector(hideCurrentPlatformApp)
187 keyEquivalent:@"h"]);
188 quitDoppelganger_.reset([[DoppelgangerMenuItem alloc]
189 initWithController:self
190 menuTag:IDC_CHROME_MENU
192 resourceId:IDS_EXIT_MAC
193 action:@selector(quitCurrentPlatformApp)
194 keyEquivalent:@"q"]);
195 newDoppelganger_.reset([[DoppelgangerMenuItem alloc]
196 initWithController:self
197 menuTag:IDC_FILE_MENU
198 itemTag:IDC_NEW_WINDOW
201 keyEquivalent:@"n"]);
202 // For apps, the "Window" part of "New Window" is dropped to match the default
203 // menu set given to Cocoa Apps.
204 [[newDoppelganger_ menuItem] setTitle:l10n_util::GetNSString(IDS_NEW_MAC)];
205 openDoppelganger_.reset([[DoppelgangerMenuItem alloc]
206 initWithController:self
207 menuTag:IDC_FILE_MENU
208 itemTag:IDC_OPEN_FILE
211 keyEquivalent:@"o"]);
212 allToFrontDoppelganger_.reset([[DoppelgangerMenuItem alloc]
213 initWithController:self
214 menuTag:IDC_WINDOW_MENU
215 itemTag:IDC_ALL_WINDOWS_FRONT
217 action:@selector(focusCurrentPlatformApp)
221 appMenuItem_.reset([[NSMenuItem alloc] initWithTitle:@""
224 base::scoped_nsobject<NSMenu> appMenu([[NSMenu alloc] initWithTitle:@""]);
225 [appMenuItem_ setSubmenu:appMenu];
226 [appMenu setAutoenablesItems:NO];
228 [appMenu addItem:[aboutDoppelganger_ menuItem]];
229 [[aboutDoppelganger_ menuItem] setEnabled:NO]; // Not implemented yet.
230 [appMenu addItem:[NSMenuItem separatorItem]];
231 [appMenu addItem:[hideDoppelganger_ menuItem]];
232 [appMenu addItem:[NSMenuItem separatorItem]];
233 [appMenu addItem:[quitDoppelganger_ menuItem]];
236 fileMenuItem_.reset([NewTopLevelItemFrom(IDC_FILE_MENU) retain]);
237 [[fileMenuItem_ submenu] addItem:[newDoppelganger_ menuItem]];
238 [[fileMenuItem_ submenu] addItem:[openDoppelganger_ menuItem]];
239 [[fileMenuItem_ submenu] addItem:[NSMenuItem separatorItem]];
240 AddDuplicateItem(fileMenuItem_, IDC_FILE_MENU, IDC_CLOSE_WINDOW);
241 // Set the expected key equivalent explicitly here because
242 // -[AppControllerMac adjustCloseWindowMenuItemKeyEquivalent:] sets it to
243 // "W" (Cmd+Shift+w) when a tabbed window has focus; it will change it back
244 // to Cmd+w when a non-tabbed window has focus.
245 NSMenuItem* closeWindowMenuItem =
246 [[fileMenuItem_ submenu] itemWithTag:IDC_CLOSE_WINDOW];
247 [closeWindowMenuItem setKeyEquivalent:@"w"];
248 [closeWindowMenuItem setKeyEquivalentModifierMask:NSCommandKeyMask];
250 // Edit menu. This copies the menu entirely and removes
251 // "Paste and Match Style" and "Find". This is because the last two items,
252 // "Start Dictation" and "Special Characters" are added by OSX, so we can't
253 // copy them explicitly.
254 editMenuItem_.reset([[[NSApp mainMenu] itemWithTag:IDC_EDIT_MENU] copy]);
255 NSMenu* editMenu = [editMenuItem_ submenu];
256 [editMenu removeItem:[editMenu
257 itemWithTag:IDC_CONTENT_CONTEXT_PASTE_AND_MATCH_STYLE]];
258 [editMenu removeItem:[editMenu itemWithTag:IDC_FIND_MENU]];
261 windowMenuItem_.reset([NewTopLevelItemFrom(IDC_WINDOW_MENU) retain]);
262 AddDuplicateItem(windowMenuItem_, IDC_WINDOW_MENU, IDC_MINIMIZE_WINDOW);
263 AddDuplicateItem(windowMenuItem_, IDC_WINDOW_MENU, IDC_MAXIMIZE_WINDOW);
264 [[windowMenuItem_ submenu] addItem:[NSMenuItem separatorItem]];
265 [[windowMenuItem_ submenu] addItem:[allToFrontDoppelganger_ menuItem]];
268 - (void)registerEventHandlers {
269 [[NSNotificationCenter defaultCenter]
271 selector:@selector(windowMainStatusChanged:)
272 name:NSWindowDidBecomeMainNotification
275 [[NSNotificationCenter defaultCenter]
277 selector:@selector(windowMainStatusChanged:)
278 name:NSWindowWillCloseNotification
282 - (void)windowMainStatusChanged:(NSNotification*)notification {
283 // A Yosemite AppKit bug causes this notification to be sent during the
284 // -dealloc for a specific NSWindow. Any autoreleases sent to that window
285 // must be drained before the window finishes -dealloc. In this method, an
286 // autorelease is sent by the invocation of [NSApp windows].
287 // http://crbug.com/406944.
288 base::mac::ScopedNSAutoreleasePool pool;
290 id window = [notification object];
291 NSString* name = [notification name];
292 if ([name isEqualToString:NSWindowDidBecomeMainNotification]) {
293 extensions::AppWindow* appWindow =
294 AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile(
297 const extensions::Extension* extension = NULL;
299 extension = appWindow->GetExtension();
302 [self addMenuItems:extension];
304 [self removeMenuItems];
305 } else if ([name isEqualToString:NSWindowWillCloseNotification]) {
306 // If there are any other windows that can become main, leave the menu. It
307 // will be changed when another window becomes main. Otherwise, restore the
309 for (NSWindow* w : [NSApp windows]) {
310 if ([w canBecomeMainWindow] && ![w isEqual:window])
314 [self removeMenuItems];
320 - (void)addMenuItems:(const extensions::Extension*)app {
321 NSString* appId = base::SysUTF8ToNSString(app->id());
322 NSString* title = base::SysUTF8ToNSString(app->name());
324 if ([appId_ isEqualToString:appId])
327 [self removeMenuItems];
328 appId_.reset([appId copy]);
330 // Hide Chrome menu items.
331 NSMenu* mainMenu = [NSApp mainMenu];
332 for (NSMenuItem* item in [mainMenu itemArray])
333 [item setHidden:YES];
335 [aboutDoppelganger_ enableForApp:app];
336 [hideDoppelganger_ enableForApp:app];
337 [quitDoppelganger_ enableForApp:app];
338 [newDoppelganger_ enableForApp:app];
339 [openDoppelganger_ enableForApp:app];
341 [appMenuItem_ setTitle:appId];
342 [[appMenuItem_ submenu] setTitle:title];
344 [mainMenu addItem:appMenuItem_];
345 [mainMenu addItem:fileMenuItem_];
346 [mainMenu addItem:editMenuItem_];
347 [mainMenu addItem:windowMenuItem_];
350 - (void)removeMenuItems {
356 NSMenu* mainMenu = [NSApp mainMenu];
357 [mainMenu removeItem:appMenuItem_];
358 [mainMenu removeItem:fileMenuItem_];
359 [mainMenu removeItem:editMenuItem_];
360 [mainMenu removeItem:windowMenuItem_];
362 // Restore the Chrome main menu bar.
363 for (NSMenuItem* item in [mainMenu itemArray])
366 [aboutDoppelganger_ disable];
367 [hideDoppelganger_ disable];
368 [quitDoppelganger_ disable];
369 [newDoppelganger_ disable];
370 [openDoppelganger_ disable];
373 - (void)quitCurrentPlatformApp {
374 extensions::AppWindow* appWindow =
375 AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile(
378 apps::ExtensionAppShimHandler::QuitAppForWindow(appWindow);
381 - (void)hideCurrentPlatformApp {
382 extensions::AppWindow* appWindow =
383 AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile(
386 apps::ExtensionAppShimHandler::HideAppForWindow(appWindow);
389 - (void)focusCurrentPlatformApp {
390 extensions::AppWindow* appWindow =
391 AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile(
394 apps::ExtensionAppShimHandler::FocusAppForWindow(appWindow);