Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / apps / app_shim_menu_controller_mac.mm
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.
4
5 #import "chrome/browser/ui/cocoa/apps/app_shim_menu_controller_mac.h"
6
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"
18
19 namespace {
20
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];
26 }
27
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]
34              action:nil
35       keyEquivalent:@""]);
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();
41 }
42
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,
46                       NSInteger menu_tag,
47                       NSInteger item_tag) {
48   base::scoped_nsobject<NSMenuItem> item(
49       [GetItemByTag(menu_tag, item_tag) copy]);
50   DCHECK(item);
51   [[top_level_item submenu] addItem:item];
52 }
53
54 }  // namespace
55
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 {
65  @private
66   base::scoped_nsobject<NSMenuItem> menuItem_;
67   base::scoped_nsobject<NSMenuItem> sourceItem_;
68   base::scoped_nsobject<NSString> sourceKeyEquivalent_;
69   int resourceId_;
70 }
71
72 @property(readonly, nonatomic) NSMenuItem* menuItem;
73
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
79                   action:(SEL)action
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.
84 - (void)disable;
85 @end
86
87 @implementation DoppelgangerMenuItem
88
89 - (NSMenuItem*)menuItem {
90   return menuItem_;
91 }
92
93 - (id)initWithController:(AppShimMenuController*)controller
94                  menuTag:(NSInteger)menuTag
95                  itemTag:(NSInteger)itemTag
96               resourceId:(int)resourceId
97                   action:(SEL)action
98            keyEquivalent:(NSString*)keyEquivalent {
99   if ((self = [super init])) {
100     sourceItem_.reset([GetItemByTag(menuTag, itemTag) retain]);
101     DCHECK(sourceItem_);
102     sourceKeyEquivalent_.reset([[sourceItem_ keyEquivalent] copy]);
103     menuItem_.reset([[NSMenuItem alloc]
104         initWithTitle:[sourceItem_ title]
105                action:action
106         keyEquivalent:keyEquivalent]);
107     [menuItem_ setTarget:controller];
108     [menuItem_ setTag:itemTag];
109     resourceId_ = resourceId;
110   }
111   return self;
112 }
113
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:@""];
121   if (!resourceId_)
122     return;
123
124   [menuItem_ setTitle:l10n_util::GetNSStringF(resourceId_,
125                                               base::UTF8ToUTF16(app->name()))];
126 }
127
128 - (void)disable {
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_];
133 }
134
135 @end
136
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;
155 @end
156
157 @implementation AppShimMenuController
158
159 - (id)init {
160   if ((self = [super init])) {
161     [self buildAppMenuItems];
162     [self registerEventHandlers];
163   }
164   return self;
165 }
166
167 - (void)dealloc {
168   [[NSNotificationCenter defaultCenter] removeObserver:self];
169   [super dealloc];
170 }
171
172 - (void)buildAppMenuItems {
173   aboutDoppelganger_.reset([[DoppelgangerMenuItem alloc]
174       initWithController:self
175                  menuTag:IDC_CHROME_MENU
176                  itemTag:IDC_ABOUT
177               resourceId:IDS_ABOUT_MAC
178                   action:nil
179            keyEquivalent:@""]);
180   hideDoppelganger_.reset([[DoppelgangerMenuItem alloc]
181       initWithController:self
182                  menuTag:IDC_CHROME_MENU
183                  itemTag:IDC_HIDE_APP
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
190                  itemTag:IDC_EXIT
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
198               resourceId:0
199                   action:nil
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
208               resourceId:0
209                   action:nil
210            keyEquivalent:@"o"]);
211   allToFrontDoppelganger_.reset([[DoppelgangerMenuItem alloc]
212       initWithController:self
213                  menuTag:IDC_WINDOW_MENU
214                  itemTag:IDC_ALL_WINDOWS_FRONT
215               resourceId:0
216                   action:@selector(focusCurrentPlatformApp)
217            keyEquivalent:@""]);
218
219   // The app's menu.
220   appMenuItem_.reset([[NSMenuItem alloc] initWithTitle:@""
221                                                 action:nil
222                                          keyEquivalent:@""]);
223   base::scoped_nsobject<NSMenu> appMenu([[NSMenu alloc] initWithTitle:@""]);
224   [appMenuItem_ setSubmenu:appMenu];
225   [appMenu setAutoenablesItems:NO];
226
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]];
233
234   // File menu.
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"];
246
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]];
256
257   // Window 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]];
263 }
264
265 - (void)registerEventHandlers {
266   [[NSNotificationCenter defaultCenter]
267       addObserver:self
268          selector:@selector(windowMainStatusChanged:)
269              name:NSWindowDidBecomeMainNotification
270            object:nil];
271
272   [[NSNotificationCenter defaultCenter]
273       addObserver:self
274          selector:@selector(windowMainStatusChanged:)
275              name:NSWindowWillCloseNotification
276            object:nil];
277 }
278
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);
285
286     const extensions::Extension* extension = NULL;
287     if (appWindow)
288       extension = appWindow->GetExtension();
289
290     if (extension)
291       [self addMenuItems:extension];
292     else
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
297     // Chrome menu.
298     for (NSWindow* w : [NSApp windows]) {
299       if ([w canBecomeMainWindow] && ![w isEqual:window])
300         return;
301     }
302
303     [self removeMenuItems];
304   } else {
305     NOTREACHED();
306   }
307 }
308
309 - (void)addMenuItems:(const extensions::Extension*)app {
310   NSString* appId = base::SysUTF8ToNSString(app->id());
311   NSString* title = base::SysUTF8ToNSString(app->name());
312
313   if ([appId_ isEqualToString:appId])
314     return;
315
316   [self removeMenuItems];
317   appId_.reset([appId copy]);
318
319   // Hide Chrome menu items.
320   NSMenu* mainMenu = [NSApp mainMenu];
321   for (NSMenuItem* item in [mainMenu itemArray])
322     [item setHidden:YES];
323
324   [aboutDoppelganger_ enableForApp:app];
325   [hideDoppelganger_ enableForApp:app];
326   [quitDoppelganger_ enableForApp:app];
327   [newDoppelganger_ enableForApp:app];
328   [openDoppelganger_ enableForApp:app];
329
330   [appMenuItem_ setTitle:appId];
331   [[appMenuItem_ submenu] setTitle:title];
332
333   [mainMenu addItem:appMenuItem_];
334   [mainMenu addItem:fileMenuItem_];
335   [mainMenu addItem:editMenuItem_];
336   [mainMenu addItem:windowMenuItem_];
337 }
338
339 - (void)removeMenuItems {
340   if (!appId_)
341     return;
342
343   appId_.reset();
344
345   NSMenu* mainMenu = [NSApp mainMenu];
346   [mainMenu removeItem:appMenuItem_];
347   [mainMenu removeItem:fileMenuItem_];
348   [mainMenu removeItem:editMenuItem_];
349   [mainMenu removeItem:windowMenuItem_];
350
351   // Restore the Chrome main menu bar.
352   for (NSMenuItem* item in [mainMenu itemArray])
353     [item setHidden:NO];
354
355   [aboutDoppelganger_ disable];
356   [hideDoppelganger_ disable];
357   [quitDoppelganger_ disable];
358   [newDoppelganger_ disable];
359   [openDoppelganger_ disable];
360 }
361
362 - (void)quitCurrentPlatformApp {
363   apps::AppWindow* appWindow =
364       apps::AppWindowRegistry::GetAppWindowForNativeWindowAnyProfile(
365           [NSApp keyWindow]);
366   if (appWindow)
367     apps::ExtensionAppShimHandler::QuitAppForWindow(appWindow);
368 }
369
370 - (void)hideCurrentPlatformApp {
371   apps::AppWindow* appWindow =
372       apps::AppWindowRegistry::GetAppWindowForNativeWindowAnyProfile(
373           [NSApp keyWindow]);
374   if (appWindow)
375     apps::ExtensionAppShimHandler::HideAppForWindow(appWindow);
376 }
377
378 - (void)focusCurrentPlatformApp {
379   apps::AppWindow* appWindow =
380       apps::AppWindowRegistry::GetAppWindowForNativeWindowAnyProfile(
381           [NSApp keyWindow]);
382   if (appWindow)
383     apps::ExtensionAppShimHandler::FocusAppForWindow(appWindow);
384 }
385
386 @end