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 20namespace { 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|. 24NSMenuItem* 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. 31NSMenuItem* 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|. 46void 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 [[[fileMenuItem_ submenu] itemWithTag:IDC_CLOSE_WINDOW] 246 setKeyEquivalent:@"w"]; 247 248 // Edit menu. This copies the menu entirely and removes 249 // "Paste and Match Style" and "Find". This is because the last two items, 250 // "Start Dictation" and "Special Characters" are added by OSX, so we can't 251 // copy them explicitly. 252 editMenuItem_.reset([[[NSApp mainMenu] itemWithTag:IDC_EDIT_MENU] copy]); 253 NSMenu* editMenu = [editMenuItem_ submenu]; 254 [editMenu removeItem:[editMenu 255 itemWithTag:IDC_CONTENT_CONTEXT_PASTE_AND_MATCH_STYLE]]; 256 [editMenu removeItem:[editMenu itemWithTag:IDC_FIND_MENU]]; 257 258 // Window menu. 259 windowMenuItem_.reset([NewTopLevelItemFrom(IDC_WINDOW_MENU) retain]); 260 AddDuplicateItem(windowMenuItem_, IDC_WINDOW_MENU, IDC_MINIMIZE_WINDOW); 261 AddDuplicateItem(windowMenuItem_, IDC_WINDOW_MENU, IDC_MAXIMIZE_WINDOW); 262 [[windowMenuItem_ submenu] addItem:[NSMenuItem separatorItem]]; 263 [[windowMenuItem_ submenu] addItem:[allToFrontDoppelganger_ menuItem]]; 264} 265 266- (void)registerEventHandlers { 267 [[NSNotificationCenter defaultCenter] 268 addObserver:self 269 selector:@selector(windowMainStatusChanged:) 270 name:NSWindowDidBecomeMainNotification 271 object:nil]; 272 273 [[NSNotificationCenter defaultCenter] 274 addObserver:self 275 selector:@selector(windowMainStatusChanged:) 276 name:NSWindowWillCloseNotification 277 object:nil]; 278} 279 280- (void)windowMainStatusChanged:(NSNotification*)notification { 281 // A Yosemite AppKit bug causes this notification to be sent during the 282 // -dealloc for a specific NSWindow. Any autoreleases sent to that window 283 // must be drained before the window finishes -dealloc. In this method, an 284 // autorelease is sent by the invocation of [NSApp windows]. 285 // http://crbug.com/406944. 286 base::mac::ScopedNSAutoreleasePool pool; 287 288 id window = [notification object]; 289 NSString* name = [notification name]; 290 if ([name isEqualToString:NSWindowDidBecomeMainNotification]) { 291 extensions::AppWindow* appWindow = 292 AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile( 293 window); 294 295 const extensions::Extension* extension = NULL; 296 if (appWindow) 297 extension = appWindow->GetExtension(); 298 299 if (extension) 300 [self addMenuItems:extension]; 301 else 302 [self removeMenuItems]; 303 } else if ([name isEqualToString:NSWindowWillCloseNotification]) { 304 // If there are any other windows that can become main, leave the menu. It 305 // will be changed when another window becomes main. Otherwise, restore the 306 // Chrome menu. 307 for (NSWindow* w : [NSApp windows]) { 308 if ([w canBecomeMainWindow] && ![w isEqual:window]) 309 return; 310 } 311 312 [self removeMenuItems]; 313 } else { 314 NOTREACHED(); 315 } 316} 317 318- (void)addMenuItems:(const extensions::Extension*)app { 319 NSString* appId = base::SysUTF8ToNSString(app->id()); 320 NSString* title = base::SysUTF8ToNSString(app->name()); 321 322 if ([appId_ isEqualToString:appId]) 323 return; 324 325 [self removeMenuItems]; 326 appId_.reset([appId copy]); 327 328 // Hide Chrome menu items. 329 NSMenu* mainMenu = [NSApp mainMenu]; 330 for (NSMenuItem* item in [mainMenu itemArray]) 331 [item setHidden:YES]; 332 333 [aboutDoppelganger_ enableForApp:app]; 334 [hideDoppelganger_ enableForApp:app]; 335 [quitDoppelganger_ enableForApp:app]; 336 [newDoppelganger_ enableForApp:app]; 337 [openDoppelganger_ enableForApp:app]; 338 339 [appMenuItem_ setTitle:appId]; 340 [[appMenuItem_ submenu] setTitle:title]; 341 342 [mainMenu addItem:appMenuItem_]; 343 [mainMenu addItem:fileMenuItem_]; 344 [mainMenu addItem:editMenuItem_]; 345 [mainMenu addItem:windowMenuItem_]; 346} 347 348- (void)removeMenuItems { 349 if (!appId_) 350 return; 351 352 appId_.reset(); 353 354 NSMenu* mainMenu = [NSApp mainMenu]; 355 [mainMenu removeItem:appMenuItem_]; 356 [mainMenu removeItem:fileMenuItem_]; 357 [mainMenu removeItem:editMenuItem_]; 358 [mainMenu removeItem:windowMenuItem_]; 359 360 // Restore the Chrome main menu bar. 361 for (NSMenuItem* item in [mainMenu itemArray]) 362 [item setHidden:NO]; 363 364 [aboutDoppelganger_ disable]; 365 [hideDoppelganger_ disable]; 366 [quitDoppelganger_ disable]; 367 [newDoppelganger_ disable]; 368 [openDoppelganger_ disable]; 369} 370 371- (void)quitCurrentPlatformApp { 372 extensions::AppWindow* appWindow = 373 AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile( 374 [NSApp keyWindow]); 375 if (appWindow) 376 apps::ExtensionAppShimHandler::QuitAppForWindow(appWindow); 377} 378 379- (void)hideCurrentPlatformApp { 380 extensions::AppWindow* appWindow = 381 AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile( 382 [NSApp keyWindow]); 383 if (appWindow) 384 apps::ExtensionAppShimHandler::HideAppForWindow(appWindow); 385} 386 387- (void)focusCurrentPlatformApp { 388 extensions::AppWindow* appWindow = 389 AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile( 390 [NSApp keyWindow]); 391 if (appWindow) 392 apps::ExtensionAppShimHandler::FocusAppForWindow(appWindow); 393} 394 395@end 396