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 "apps/app_shim/extension_app_shim_handler_mac.h"
8 #include "apps/app_window.h"
9 #include "apps/app_window_registry.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/app/chrome_command_ids.h"
13 #import "chrome/browser/ui/cocoa/apps/native_app_window_cocoa.h"
14 #include "extensions/common/extension.h"
15 #include "grit/generated_resources.h"
16 #include "ui/base/l10n/l10n_util.h"
17 #include "ui/base/l10n/l10n_util_mac.h"
21 // Gets an item from the main menu given the tag of the top level item
22 // |menu_tag| and the tag of the item |item_tag|.
23 NSMenuItem* GetItemByTag(NSInteger menu_tag, NSInteger item_tag) {
24 return [[[[NSApp mainMenu] itemWithTag:menu_tag] submenu]
25 itemWithTag:item_tag];
28 // Finds a top level menu item using |menu_tag| and creates a new NSMenuItem
29 // with the same title.
30 NSMenuItem* NewTopLevelItemFrom(NSInteger menu_tag) {
31 NSMenuItem* original = [[NSApp mainMenu] itemWithTag:menu_tag];
32 base::scoped_nsobject<NSMenuItem> item([[NSMenuItem alloc]
33 initWithTitle:[original title]
36 DCHECK([original hasSubmenu]);
37 base::scoped_nsobject<NSMenu> sub_menu([[NSMenu alloc]
38 initWithTitle:[[original submenu] title]]);
39 [item setSubmenu:sub_menu];
40 return item.autorelease();
43 // Finds an item using |menu_tag| and |item_tag| and adds a duplicate of it to
44 // the submenu of |top_level_item|.
45 void AddDuplicateItem(NSMenuItem* top_level_item,
48 base::scoped_nsobject<NSMenuItem> item(
49 [GetItemByTag(menu_tag, item_tag) copy]);
51 [[top_level_item submenu] addItem:item];
56 // Used by AppShimMenuController to manage menu items that are a copy of a
57 // Chrome menu item but with a different action. This manages unsetting and
58 // restoring the original item's key equivalent, so that we can use the same
59 // key equivalent in the copied item with a different action. If |resourceId_|
60 // is non-zero, this will also update the title to include the app name.
61 // If the copy (menuItem) has no key equivalent, and the title does not have the
62 // app name, then enableForApp and disable do not need to be called. I.e. the
63 // doppelganger just copies the item and sets a new action.
64 @interface DoppelgangerMenuItem : NSObject {
66 base::scoped_nsobject<NSMenuItem> menuItem_;
67 base::scoped_nsobject<NSMenuItem> sourceItem_;
68 base::scoped_nsobject<NSString> sourceKeyEquivalent_;
72 @property(readonly, nonatomic) NSMenuItem* menuItem;
74 // Get the source item using the tags and create the menu item.
75 - (id)initWithController:(AppShimMenuController*)controller
76 menuTag:(NSInteger)menuTag
77 itemTag:(NSInteger)itemTag
78 resourceId:(int)resourceId
80 keyEquivalent:(NSString*)keyEquivalent;
81 // Set the title using |resourceId_| and unset the source item's key equivalent.
82 - (void)enableForApp:(const extensions::Extension*)app;
83 // Restore the source item's key equivalent.
87 @implementation DoppelgangerMenuItem
89 - (NSMenuItem*)menuItem {
93 - (id)initWithController:(AppShimMenuController*)controller
94 menuTag:(NSInteger)menuTag
95 itemTag:(NSInteger)itemTag
96 resourceId:(int)resourceId
98 keyEquivalent:(NSString*)keyEquivalent {
99 if ((self = [super init])) {
100 sourceItem_.reset([GetItemByTag(menuTag, itemTag) retain]);
102 sourceKeyEquivalent_.reset([[sourceItem_ keyEquivalent] copy]);
103 menuItem_.reset([[NSMenuItem alloc]
104 initWithTitle:[sourceItem_ title]
106 keyEquivalent:keyEquivalent]);
107 [menuItem_ setTarget:controller];
108 [menuItem_ setTag:itemTag];
109 resourceId_ = resourceId;
114 - (void)enableForApp:(const extensions::Extension*)app {
115 // It seems that two menu items that have the same key equivalent must also
116 // have the same action for the keyboard shortcut to work. (This refers to the
117 // original keyboard shortcut, regardless of any overrides set in OSX).
118 // In order to let the app menu items have a different action, we remove the
119 // key equivalent of the original items and restore them later.
120 [sourceItem_ setKeyEquivalent:@""];
124 [menuItem_ setTitle:l10n_util::GetNSStringF(resourceId_,
125 base::UTF8ToUTF16(app->name()))];
129 // Restore the keyboard shortcut to Chrome. This just needs to be set back to
130 // the original keyboard shortcut, regardless of any overrides in OSX. The
131 // overrides still work as they are based on the title of the menu item.
132 [sourceItem_ setKeyEquivalent:sourceKeyEquivalent_];
137 @interface AppShimMenuController ()
138 // Construct the NSMenuItems for apps.
139 - (void)buildAppMenuItems;
140 // Register for NSWindow notifications.
141 - (void)registerEventHandlers;
142 // If the window is an app window, add or remove menu items.
143 - (void)windowMainStatusChanged:(NSNotification*)notification;
144 // Add menu items for an app and hide Chrome menu items.
145 - (void)addMenuItems:(const extensions::Extension*)app;
146 // If the window belongs to the currently focused app, remove the menu items and
147 // unhide Chrome menu items.
148 - (void)removeMenuItems;
149 // If the currently focused window belongs to a platform app, quit the app.
150 - (void)quitCurrentPlatformApp;
151 // If the currently focused window belongs to a platform app, hide the app.
152 - (void)hideCurrentPlatformApp;
153 // If the currently focused window belongs to a platform app, focus the app.
154 - (void)focusCurrentPlatformApp;
157 @implementation AppShimMenuController
160 if ((self = [super init])) {
161 [self buildAppMenuItems];
162 [self registerEventHandlers];
168 [[NSNotificationCenter defaultCenter] removeObserver:self];
172 - (void)buildAppMenuItems {
173 aboutDoppelganger_.reset([[DoppelgangerMenuItem alloc]
174 initWithController:self
175 menuTag:IDC_CHROME_MENU
177 resourceId:IDS_ABOUT_MAC
180 hideDoppelganger_.reset([[DoppelgangerMenuItem alloc]
181 initWithController:self
182 menuTag:IDC_CHROME_MENU
184 resourceId:IDS_HIDE_APP_MAC
185 action:@selector(hideCurrentPlatformApp)
186 keyEquivalent:@"h"]);
187 quitDoppelganger_.reset([[DoppelgangerMenuItem alloc]
188 initWithController:self
189 menuTag:IDC_CHROME_MENU
191 resourceId:IDS_EXIT_MAC
192 action:@selector(quitCurrentPlatformApp)
193 keyEquivalent:@"q"]);
194 newDoppelganger_.reset([[DoppelgangerMenuItem alloc]
195 initWithController:self
196 menuTag:IDC_FILE_MENU
197 itemTag:IDC_NEW_WINDOW
200 keyEquivalent:@"n"]);
201 // For apps, the "Window" part of "New Window" is dropped to match the default
202 // menu set given to Cocoa Apps.
203 [[newDoppelganger_ menuItem] setTitle:l10n_util::GetNSString(IDS_NEW_MAC)];
204 openDoppelganger_.reset([[DoppelgangerMenuItem alloc]
205 initWithController:self
206 menuTag:IDC_FILE_MENU
207 itemTag:IDC_OPEN_FILE
210 keyEquivalent:@"o"]);
211 allToFrontDoppelganger_.reset([[DoppelgangerMenuItem alloc]
212 initWithController:self
213 menuTag:IDC_WINDOW_MENU
214 itemTag:IDC_ALL_WINDOWS_FRONT
216 action:@selector(focusCurrentPlatformApp)
220 appMenuItem_.reset([[NSMenuItem alloc] initWithTitle:@""
223 base::scoped_nsobject<NSMenu> appMenu([[NSMenu alloc] initWithTitle:@""]);
224 [appMenuItem_ setSubmenu:appMenu];
225 [appMenu setAutoenablesItems:NO];
227 [appMenu addItem:[aboutDoppelganger_ menuItem]];
228 [[aboutDoppelganger_ menuItem] setEnabled:NO]; // Not implemented yet.
229 [appMenu addItem:[NSMenuItem separatorItem]];
230 [appMenu addItem:[hideDoppelganger_ menuItem]];
231 [appMenu addItem:[NSMenuItem separatorItem]];
232 [appMenu addItem:[quitDoppelganger_ menuItem]];
235 fileMenuItem_.reset([NewTopLevelItemFrom(IDC_FILE_MENU) retain]);
236 [[fileMenuItem_ submenu] addItem:[newDoppelganger_ menuItem]];
237 [[fileMenuItem_ submenu] addItem:[openDoppelganger_ menuItem]];
238 [[fileMenuItem_ submenu] addItem:[NSMenuItem separatorItem]];
239 AddDuplicateItem(fileMenuItem_, IDC_FILE_MENU, IDC_CLOSE_WINDOW);
240 // Set the expected key equivalent explicitly here because
241 // -[AppControllerMac adjustCloseWindowMenuItemKeyEquivalent:] sets it to
242 // "W" (Cmd+Shift+w) when a tabbed window has focus; it will change it back
243 // to Cmd+w when a non-tabbed window has focus.
244 [[[fileMenuItem_ submenu] itemWithTag:IDC_CLOSE_WINDOW]
245 setKeyEquivalent:@"w"];
247 // Edit menu. This copies the menu entirely and removes
248 // "Paste and Match Style" and "Find". This is because the last two items,
249 // "Start Dictation" and "Special Characters" are added by OSX, so we can't
250 // copy them explicitly.
251 editMenuItem_.reset([[[NSApp mainMenu] itemWithTag:IDC_EDIT_MENU] copy]);
252 NSMenu* editMenu = [editMenuItem_ submenu];
253 [editMenu removeItem:[editMenu
254 itemWithTag:IDC_CONTENT_CONTEXT_PASTE_AND_MATCH_STYLE]];
255 [editMenu removeItem:[editMenu itemWithTag:IDC_FIND_MENU]];
258 windowMenuItem_.reset([NewTopLevelItemFrom(IDC_WINDOW_MENU) retain]);
259 AddDuplicateItem(windowMenuItem_, IDC_WINDOW_MENU, IDC_MINIMIZE_WINDOW);
260 AddDuplicateItem(windowMenuItem_, IDC_WINDOW_MENU, IDC_MAXIMIZE_WINDOW);
261 [[windowMenuItem_ submenu] addItem:[NSMenuItem separatorItem]];
262 [[windowMenuItem_ submenu] addItem:[allToFrontDoppelganger_ menuItem]];
265 - (void)registerEventHandlers {
266 [[NSNotificationCenter defaultCenter]
268 selector:@selector(windowMainStatusChanged:)
269 name:NSWindowDidBecomeMainNotification
272 [[NSNotificationCenter defaultCenter]
274 selector:@selector(windowMainStatusChanged:)
275 name:NSWindowWillCloseNotification
279 - (void)windowMainStatusChanged:(NSNotification*)notification {
280 id window = [notification object];
281 NSString* name = [notification name];
282 if ([name isEqualToString:NSWindowDidBecomeMainNotification]) {
283 apps::AppWindow* appWindow =
284 apps::AppWindowRegistry::GetAppWindowForNativeWindowAnyProfile(window);
286 const extensions::Extension* extension = NULL;
288 extension = appWindow->GetExtension();
291 [self addMenuItems:extension];
293 [self removeMenuItems];
294 } else if ([name isEqualToString:NSWindowWillCloseNotification]) {
295 // If there are any other windows that can become main, leave the menu. It
296 // will be changed when another window becomes main. Otherwise, restore the
298 for (NSWindow* w : [NSApp windows]) {
299 if ([w canBecomeMainWindow] && ![w isEqual:window])
303 [self removeMenuItems];
309 - (void)addMenuItems:(const extensions::Extension*)app {
310 NSString* appId = base::SysUTF8ToNSString(app->id());
311 NSString* title = base::SysUTF8ToNSString(app->name());
313 if ([appId_ isEqualToString:appId])
316 [self removeMenuItems];
317 appId_.reset([appId copy]);
319 // Hide Chrome menu items.
320 NSMenu* mainMenu = [NSApp mainMenu];
321 for (NSMenuItem* item in [mainMenu itemArray])
322 [item setHidden:YES];
324 [aboutDoppelganger_ enableForApp:app];
325 [hideDoppelganger_ enableForApp:app];
326 [quitDoppelganger_ enableForApp:app];
327 [newDoppelganger_ enableForApp:app];
328 [openDoppelganger_ enableForApp:app];
330 [appMenuItem_ setTitle:appId];
331 [[appMenuItem_ submenu] setTitle:title];
333 [mainMenu addItem:appMenuItem_];
334 [mainMenu addItem:fileMenuItem_];
335 [mainMenu addItem:editMenuItem_];
336 [mainMenu addItem:windowMenuItem_];
339 - (void)removeMenuItems {
345 NSMenu* mainMenu = [NSApp mainMenu];
346 [mainMenu removeItem:appMenuItem_];
347 [mainMenu removeItem:fileMenuItem_];
348 [mainMenu removeItem:editMenuItem_];
349 [mainMenu removeItem:windowMenuItem_];
351 // Restore the Chrome main menu bar.
352 for (NSMenuItem* item in [mainMenu itemArray])
355 [aboutDoppelganger_ disable];
356 [hideDoppelganger_ disable];
357 [quitDoppelganger_ disable];
358 [newDoppelganger_ disable];
359 [openDoppelganger_ disable];
362 - (void)quitCurrentPlatformApp {
363 apps::AppWindow* appWindow =
364 apps::AppWindowRegistry::GetAppWindowForNativeWindowAnyProfile(
367 apps::ExtensionAppShimHandler::QuitAppForWindow(appWindow);
370 - (void)hideCurrentPlatformApp {
371 apps::AppWindow* appWindow =
372 apps::AppWindowRegistry::GetAppWindowForNativeWindowAnyProfile(
375 apps::ExtensionAppShimHandler::HideAppForWindow(appWindow);
378 - (void)focusCurrentPlatformApp {
379 apps::AppWindow* appWindow =
380 apps::AppWindowRegistry::GetAppWindowForNativeWindowAnyProfile(
383 apps::ExtensionAppShimHandler::FocusAppForWindow(appWindow);