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