app_controller_mac.mm revision 3345a6884c488ff3a535c2c9acdd33d74b37e311
1// Copyright (c) 2010 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/app_controller_mac.h" 6 7#include "app/l10n_util.h" 8#include "app/l10n_util_mac.h" 9#include "base/auto_reset.h" 10#include "base/command_line.h" 11#include "base/file_path.h" 12#include "base/mac_util.h" 13#include "base/message_loop.h" 14#include "base/string_number_conversions.h" 15#include "base/sys_string_conversions.h" 16#include "chrome/app/chrome_dll_resource.h" 17#include "chrome/browser/browser.h" 18#include "chrome/browser/browser_init.h" 19#include "chrome/browser/browser_list.h" 20#include "chrome/browser/browser_process.h" 21#include "chrome/browser/browser_shutdown.h" 22#include "chrome/browser/browser_window.h" 23#include "chrome/browser/chrome_thread.h" 24#import "chrome/browser/cocoa/about_window_controller.h" 25#import "chrome/browser/cocoa/bookmark_menu_bridge.h" 26#import "chrome/browser/cocoa/browser_window_cocoa.h" 27#import "chrome/browser/cocoa/browser_window_controller.h" 28#import "chrome/browser/cocoa/bug_report_window_controller.h" 29#import "chrome/browser/cocoa/clear_browsing_data_controller.h" 30#import "chrome/browser/cocoa/encoding_menu_controller_delegate_mac.h" 31#import "chrome/browser/cocoa/history_menu_bridge.h" 32#import "chrome/browser/cocoa/import_settings_dialog.h" 33#import "chrome/browser/cocoa/preferences_window_controller.h" 34#import "chrome/browser/cocoa/tab_strip_controller.h" 35#import "chrome/browser/cocoa/tab_window_controller.h" 36#include "chrome/browser/cocoa/task_manager_mac.h" 37#include "chrome/browser/command_updater.h" 38#include "chrome/browser/download/download_manager.h" 39#include "chrome/browser/metrics/user_metrics.h" 40#include "chrome/browser/options_window.h" 41#include "chrome/browser/prefs/pref_service.h" 42#include "chrome/browser/profile_manager.h" 43#include "chrome/browser/sessions/tab_restore_service.h" 44#include "chrome/browser/sync/profile_sync_service.h" 45#include "chrome/browser/sync/sync_ui_util.h" 46#include "chrome/browser/sync/sync_ui_util_mac.h" 47#include "chrome/browser/tab_contents/tab_contents.h" 48#include "chrome/common/app_mode_common_mac.h" 49#include "chrome/common/chrome_paths_internal.h" 50#include "chrome/common/chrome_switches.h" 51#include "chrome/common/notification_service.h" 52#include "chrome/common/pref_names.h" 53#include "chrome/common/url_constants.h" 54#include "grit/chromium_strings.h" 55#include "grit/generated_resources.h" 56#include "net/base/net_util.h" 57 58// 10.6 adds a public API for the Spotlight-backed search menu item in the Help 59// menu. Provide the declaration so it can be called below when building with 60// the 10.5 SDK. 61#if !defined(MAC_OS_X_VERSION_10_6) || \ 62 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_6 63@interface NSApplication (SnowLeopardSDKDeclarations) 64- (void)setHelpMenu:(NSMenu*)helpMenu; 65@end 66#endif 67 68namespace { 69 70// True while AppController is calling Browser::OpenEmptyWindow(). We need a 71// global flag here, analogue to BrowserInit::InProcessStartup() because 72// otherwise the SessionService will try to restore sessions when we make a new 73// window while there are no other active windows. 74bool g_is_opening_new_window = false; 75 76// Activates a browser window having the given profile (the last one active) if 77// possible and returns a pointer to the activate |Browser| or NULL if this was 78// not possible. If the last active browser is minimized (in particular, if 79// there are only minimized windows), it will unminimize it. 80Browser* ActivateBrowser(Profile* profile) { 81 Browser* browser = BrowserList::GetLastActiveWithProfile(profile); 82 if (browser) 83 browser->window()->Activate(); 84 return browser; 85} 86 87// Creates an empty browser window with the given profile and returns a pointer 88// to the new |Browser|. 89Browser* CreateBrowser(Profile* profile) { 90 { 91 AutoReset<bool> auto_reset_in_run(&g_is_opening_new_window, true); 92 Browser::OpenEmptyWindow(profile); 93 } 94 95 Browser* browser = BrowserList::GetLastActive(); 96 CHECK(browser); 97 return browser; 98} 99 100// Activates a browser window having the given profile (the last one active) if 101// possible or creates an empty one if necessary. Returns a pointer to the 102// activated/new |Browser|. 103Browser* ActivateOrCreateBrowser(Profile* profile) { 104 if (Browser* browser = ActivateBrowser(profile)) 105 return browser; 106 return CreateBrowser(profile); 107} 108 109// This task synchronizes preferences (under "org.chromium.Chromium" or 110// "com.google.Chrome"), in particular, writes them out to disk. 111class PrefsSyncTask : public Task { 112 public: 113 PrefsSyncTask() {} 114 virtual ~PrefsSyncTask() {} 115 virtual void Run() { 116 if (!CFPreferencesAppSynchronize(app_mode::kAppPrefsID)) 117 LOG(WARNING) << "Error recording application bundle path."; 118 } 119}; 120 121// Record the location of the application bundle (containing the main framework) 122// from which Chromium was loaded. This is used by app mode shims to find 123// Chromium. 124void RecordLastRunAppBundlePath() { 125 // Going up three levels from |chrome::GetVersionedDirectory()| gives the 126 // real, user-visible app bundle directory. (The alternatives give either the 127 // framework's path or the initial app's path, which may be an app mode shim 128 // or a unit test.) 129 FilePath appBundlePath = 130 chrome::GetVersionedDirectory().DirName().DirName().DirName(); 131 CFPreferencesSetAppValue(app_mode::kLastRunAppBundlePathPrefsKey, 132 base::SysUTF8ToCFStringRef(appBundlePath.value()), 133 app_mode::kAppPrefsID); 134 135 // Sync after a delay avoid I/O contention on startup; 1500 ms is plenty. 136 ChromeThread::PostDelayedTask(ChromeThread::FILE, FROM_HERE, 137 new PrefsSyncTask(), 1500); 138} 139 140} // anonymous namespace 141 142@interface AppController(Private) 143- (void)initMenuState; 144- (void)registerServicesMenuTypesTo:(NSApplication*)app; 145- (void)openUrls:(const std::vector<GURL>&)urls; 146- (void)getUrl:(NSAppleEventDescriptor*)event 147 withReply:(NSAppleEventDescriptor*)reply; 148- (void)windowLayeringDidChange:(NSNotification*)inNotification; 149- (void)checkForAnyKeyWindows; 150- (BOOL)userWillWaitForInProgressDownloads:(int)downloadCount; 151- (BOOL)shouldQuitWithInProgressDownloads; 152- (void)showPreferencesWindow:(id)sender 153 page:(OptionsPage)page 154 profile:(Profile*)profile; 155@end 156 157@implementation AppController 158 159@synthesize startupComplete = startupComplete_; 160 161// This method is called very early in application startup (ie, before 162// the profile is loaded or any preferences have been registered). Defer any 163// user-data initialization until -applicationDidFinishLaunching:. 164- (void)awakeFromNib { 165 // We need to register the handlers early to catch events fired on launch. 166 NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager]; 167 [em setEventHandler:self 168 andSelector:@selector(getUrl:withReply:) 169 forEventClass:kInternetEventClass 170 andEventID:kAEGetURL]; 171 [em setEventHandler:self 172 andSelector:@selector(getUrl:withReply:) 173 forEventClass:'WWW!' // A particularly ancient AppleEvent that dates 174 andEventID:'OURL']; // back to the Spyglass days. 175 176 // Register for various window layering changes. We use these to update 177 // various UI elements (command-key equivalents, etc) when the frontmost 178 // window changes. 179 NSNotificationCenter* notificationCenter = 180 [NSNotificationCenter defaultCenter]; 181 [notificationCenter 182 addObserver:self 183 selector:@selector(windowLayeringDidChange:) 184 name:NSWindowDidBecomeKeyNotification 185 object:nil]; 186 [notificationCenter 187 addObserver:self 188 selector:@selector(windowLayeringDidChange:) 189 name:NSWindowDidResignKeyNotification 190 object:nil]; 191 [notificationCenter 192 addObserver:self 193 selector:@selector(windowLayeringDidChange:) 194 name:NSWindowDidBecomeMainNotification 195 object:nil]; 196 [notificationCenter 197 addObserver:self 198 selector:@selector(windowLayeringDidChange:) 199 name:NSWindowDidResignMainNotification 200 object:nil]; 201 202 // Register for a notification that the number of tabs changes in windows 203 // so we can adjust the close tab/window command keys. 204 [notificationCenter 205 addObserver:self 206 selector:@selector(tabsChanged:) 207 name:kTabStripNumberOfTabsChanged 208 object:nil]; 209 210 // Set up the command updater for when there are no windows open 211 [self initMenuState]; 212 213 // Activate (bring to foreground) if asked to do so. On 214 // Windows this logic isn't necessary since 215 // BrowserWindow::Activate() calls ::SetForegroundWindow() which is 216 // adequate. On Mac, BrowserWindow::Activate() calls -[NSWindow 217 // makeKeyAndOrderFront:] which does not activate the application 218 // itself. 219 const CommandLine& parsed_command_line = *CommandLine::ForCurrentProcess(); 220 if (parsed_command_line.HasSwitch(switches::kActivateOnLaunch)) { 221 [NSApp activateIgnoringOtherApps:YES]; 222 } 223} 224 225// (NSApplicationDelegate protocol) This is the Apple-approved place to override 226// the default handlers. 227- (void)applicationWillFinishLaunching:(NSNotification*)notification { 228 // Nothing here right now. 229} 230 231- (BOOL)tryToTerminateApplication:(NSApplication*)app { 232 // Check for in-process downloads, and prompt the user if they really want 233 // to quit (and thus cancel downloads). Only check if we're not already 234 // shutting down, else the user might be prompted multiple times if the 235 // download isn't stopped before terminate is called again. 236 if (!browser_shutdown::IsTryingToQuit() && 237 ![self shouldQuitWithInProgressDownloads]) 238 return NO; 239 240 // TODO(viettrungluu): Remove Apple Event handlers here? (It's safe to leave 241 // them in, but I'm not sure about UX; we'd also want to disable other things 242 // though.) http://crbug.com/40861 243 244 size_t num_browsers = BrowserList::size(); 245 246 // Initiate a shutdown (via BrowserList::CloseAllBrowsers()) if we aren't 247 // already shutting down. 248 if (!browser_shutdown::IsTryingToQuit()) 249 BrowserList::CloseAllBrowsers(); 250 251 return num_browsers == 0 ? YES : NO; 252} 253 254- (void)stopTryingToTerminateApplication:(NSApplication*)app { 255 if (browser_shutdown::IsTryingToQuit()) { 256 // Reset the "trying to quit" state, so that closing all browser windows 257 // will no longer lead to termination. 258 browser_shutdown::SetTryingToQuit(false); 259 260 // TODO(viettrungluu): Were we to remove Apple Event handlers above, we 261 // would have to reinstall them here. http://crbug.com/40861 262 } 263} 264 265// Called when the app is shutting down. Clean-up as appropriate. 266- (void)applicationWillTerminate:(NSNotification*)aNotification { 267 NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager]; 268 [em removeEventHandlerForEventClass:kInternetEventClass 269 andEventID:kAEGetURL]; 270 [em removeEventHandlerForEventClass:'WWW!' 271 andEventID:'OURL']; 272 273 // There better be no browser windows left at this point. 274 CHECK_EQ(BrowserList::size(), 0u); 275 276 // Tell BrowserList not to keep the browser process alive. Once all the 277 // browsers get dealloc'd, it will stop the RunLoop and fall back into main(). 278 BrowserList::EndKeepAlive(); 279 280 // Close these off if they have open windows. 281 [prefsController_ close]; 282 [aboutController_ close]; 283 284 [[NSNotificationCenter defaultCenter] removeObserver:self]; 285} 286 287- (void)didEndMainMessageLoop { 288 DCHECK(!BrowserList::HasBrowserWithProfile([self defaultProfile])); 289 if (!BrowserList::HasBrowserWithProfile([self defaultProfile])) { 290 // As we're shutting down, we need to nuke the TabRestoreService, which 291 // will start the shutdown of the NavigationControllers and allow for 292 // proper shutdown. If we don't do this, Chrome won't shut down cleanly, 293 // and may end up crashing when some thread tries to use the IO thread (or 294 // another thread) that is no longer valid. 295 [self defaultProfile]->ResetTabRestoreService(); 296 } 297} 298 299// Helper routine to get the window controller if the key window is a tabbed 300// window, or nil if not. Examples of non-tabbed windows are "about" or 301// "preferences". 302- (TabWindowController*)keyWindowTabController { 303 NSWindowController* keyWindowController = 304 [[NSApp keyWindow] windowController]; 305 if ([keyWindowController isKindOfClass:[TabWindowController class]]) 306 return (TabWindowController*)keyWindowController; 307 308 return nil; 309} 310 311// Helper routine to get the window controller if the main window is a tabbed 312// window, or nil if not. Examples of non-tabbed windows are "about" or 313// "preferences". 314- (TabWindowController*)mainWindowTabController { 315 NSWindowController* mainWindowController = 316 [[NSApp mainWindow] windowController]; 317 if ([mainWindowController isKindOfClass:[TabWindowController class]]) 318 return (TabWindowController*)mainWindowController; 319 320 return nil; 321} 322 323// If the window has tabs, make "close window" be cmd-shift-w, otherwise leave 324// it as the normal cmd-w. Capitalization of the key equivalent affects whether 325// the shift modifer is used. 326- (void)adjustCloseWindowMenuItemKeyEquivalent:(BOOL)inHaveTabs { 327 [closeWindowMenuItem_ setKeyEquivalent:(inHaveTabs ? @"W" : @"w")]; 328 [closeWindowMenuItem_ setKeyEquivalentModifierMask:NSCommandKeyMask]; 329} 330 331// If the window has tabs, make "close tab" take over cmd-w, otherwise it 332// shouldn't have any key-equivalent because it should be disabled. 333- (void)adjustCloseTabMenuItemKeyEquivalent:(BOOL)hasTabs { 334 if (hasTabs) { 335 [closeTabMenuItem_ setKeyEquivalent:@"w"]; 336 [closeTabMenuItem_ setKeyEquivalentModifierMask:NSCommandKeyMask]; 337 } else { 338 [closeTabMenuItem_ setKeyEquivalent:@""]; 339 [closeTabMenuItem_ setKeyEquivalentModifierMask:0]; 340 } 341} 342 343// Explicitly remove any command-key equivalents from the close tab/window 344// menus so that nothing can go haywire if we get a user action during pending 345// updates. 346- (void)clearCloseMenuItemKeyEquivalents { 347 [closeTabMenuItem_ setKeyEquivalent:@""]; 348 [closeTabMenuItem_ setKeyEquivalentModifierMask:0]; 349 [closeWindowMenuItem_ setKeyEquivalent:@""]; 350 [closeWindowMenuItem_ setKeyEquivalentModifierMask:0]; 351} 352 353// See if we have a window with tabs open, and adjust the key equivalents for 354// Close Tab/Close Window accordingly. 355- (void)fixCloseMenuItemKeyEquivalents { 356 fileMenuUpdatePending_ = NO; 357 TabWindowController* tabController = [self keyWindowTabController]; 358 if (!tabController && ![NSApp keyWindow]) { 359 // There might be a small amount of time where there is no key window, 360 // so just use our main browser window if there is one. 361 tabController = [self mainWindowTabController]; 362 } 363 BOOL windowWithMultipleTabs = 364 (tabController && [tabController numberOfTabs] > 1); 365 [self adjustCloseWindowMenuItemKeyEquivalent:windowWithMultipleTabs]; 366 [self adjustCloseTabMenuItemKeyEquivalent:windowWithMultipleTabs]; 367} 368 369// Fix up the "close tab/close window" command-key equivalents. We do this 370// after a delay to ensure that window layer state has been set by the time 371// we do the enabling. This should only be called on the main thread, code that 372// calls this (even as a side-effect) from other threads needs to be fixed. 373- (void)delayedFixCloseMenuItemKeyEquivalents { 374 DCHECK([NSThread isMainThread]); 375 if (!fileMenuUpdatePending_) { 376 // The OS prefers keypresses to timers, so it's possible that a cmd-w 377 // can sneak in before this timer fires. In order to prevent that from 378 // having any bad consequences, just clear the keys combos altogether. They 379 // will be reset when the timer eventually fires. 380 if ([NSThread isMainThread]) { 381 fileMenuUpdatePending_ = YES; 382 [self clearCloseMenuItemKeyEquivalents]; 383 [self performSelector:@selector(fixCloseMenuItemKeyEquivalents) 384 withObject:nil 385 afterDelay:0]; 386 } else { 387 // This shouldn't be happening, but if it does, force it to the main 388 // thread to avoid dropping the update. Don't mess with 389 // |fileMenuUpdatePending_| as it's not expected to be threadsafe and 390 // there could be a race between the selector finishing and setting the 391 // flag. 392 [self 393 performSelectorOnMainThread:@selector(fixCloseMenuItemKeyEquivalents) 394 withObject:nil 395 waitUntilDone:NO]; 396 } 397 } 398} 399 400// Called when we get a notification about the window layering changing to 401// update the UI based on the new main window. 402- (void)windowLayeringDidChange:(NSNotification*)notify { 403 [self delayedFixCloseMenuItemKeyEquivalents]; 404 405 if ([notify name] == NSWindowDidResignKeyNotification) { 406 // If a window is closed, this notification is fired but |[NSApp keyWindow]| 407 // returns nil regardless of whether any suitable candidates for the key 408 // window remain. It seems that the new key window for the app is not set 409 // until after this notification is fired, so a check is performed after the 410 // run loop is allowed to spin. 411 [self performSelector:@selector(checkForAnyKeyWindows) 412 withObject:nil 413 afterDelay:0.0]; 414 } 415} 416 417- (void)checkForAnyKeyWindows { 418 if ([NSApp keyWindow]) 419 return; 420 421 NotificationService::current()->Notify( 422 NotificationType::NO_KEY_WINDOW, 423 NotificationService::AllSources(), 424 NotificationService::NoDetails()); 425} 426 427// Called when the number of tabs changes in one of the browser windows. The 428// object is the tab strip controller, but we don't currently care. 429- (void)tabsChanged:(NSNotification*)notify { 430 // We don't need to do this on a delay, as in the method above, because the 431 // window layering isn't changing. As a result, there's no chance that a 432 // different window will sneak in as the key window and cause the problems 433 // we hacked around above by clearing the key equivalents. 434 [self fixCloseMenuItemKeyEquivalents]; 435} 436 437// If the auto-update interval is not set, make it 5 hours. 438// This code is specific to Mac Chrome Dev Channel. 439// Placed here for 2 reasons: 440// 1) Same spot as other Pref stuff 441// 2) Try and be friendly by keeping this after app launch 442// TODO(jrg): remove once we go Beta. 443- (void)setUpdateCheckInterval { 444#if defined(GOOGLE_CHROME_BUILD) 445 CFStringRef app = (CFStringRef)@"com.google.Keystone.Agent"; 446 CFStringRef checkInterval = (CFStringRef)@"checkInterval"; 447 CFPropertyListRef plist = CFPreferencesCopyAppValue(checkInterval, app); 448 if (!plist) { 449 const float fiveHoursInSeconds = 5.0 * 60.0 * 60.0; 450 NSNumber* value = [NSNumber numberWithFloat:fiveHoursInSeconds]; 451 CFPreferencesSetAppValue(checkInterval, value, app); 452 CFPreferencesAppSynchronize(app); 453 } 454#endif 455} 456 457// This is called after profiles have been loaded and preferences registered. 458// It is safe to access the default profile here. 459- (void)applicationDidFinishLaunching:(NSNotification*)notify { 460 // Notify BrowserList to keep the application running so it doesn't go away 461 // when all the browser windows get closed. 462 BrowserList::StartKeepAlive(); 463 464 bookmarkMenuBridge_.reset(new BookmarkMenuBridge([self defaultProfile])); 465 historyMenuBridge_.reset(new HistoryMenuBridge([self defaultProfile])); 466 467 [self setUpdateCheckInterval]; 468 469 // Build up the encoding menu, the order of the items differs based on the 470 // current locale (see http://crbug.com/7647 for details). 471 // We need a valid g_browser_process to get the profile which is why we can't 472 // call this from awakeFromNib. 473 NSMenu* view_menu = [[[NSApp mainMenu] itemWithTag:IDC_VIEW_MENU] submenu]; 474 NSMenuItem* encoding_menu_item = [view_menu itemWithTag:IDC_ENCODING_MENU]; 475 NSMenu* encoding_menu = [encoding_menu_item submenu]; 476 EncodingMenuControllerDelegate::BuildEncodingMenu([self defaultProfile], 477 encoding_menu); 478 479 // Since Chrome is localized to more languages than the OS, tell Cocoa which 480 // menu is the Help so it can add the search item to it. 481 if (helpMenu_ && [NSApp respondsToSelector:@selector(setHelpMenu:)]) 482 [NSApp setHelpMenu:helpMenu_]; 483 484 // Record the path to the (browser) app bundle; this is used by the app mode 485 // shim. 486 RecordLastRunAppBundlePath(); 487 488 // Makes "Services" menu items available. 489 [self registerServicesMenuTypesTo:[notify object]]; 490 491 startupComplete_ = YES; 492 493 // TODO(viettrungluu): This is very temporary, since this should be done "in" 494 // |BrowserMain()|, i.e., this list of startup URLs should be appended to the 495 // (probably-empty) list of URLs from the command line. 496 if (startupUrls_.size()) { 497 [self openUrls:startupUrls_]; 498 [self clearStartupUrls]; 499 } 500 501 const CommandLine& parsed_command_line = *CommandLine::ForCurrentProcess(); 502 if (!parsed_command_line.HasSwitch(switches::kEnableExposeForTabs)) { 503 [tabposeMenuItem_ setHidden:YES]; 504 } 505} 506 507// This is called after profiles have been loaded and preferences registered. 508// It is safe to access the default profile here. 509- (void)applicationDidBecomeActive:(NSNotification*)notify { 510 NotificationService::current()->Notify(NotificationType::APP_ACTIVATED, 511 NotificationService::AllSources(), 512 NotificationService::NoDetails()); 513} 514 515// Helper function for populating and displaying the in progress downloads at 516// exit alert panel. 517- (BOOL)userWillWaitForInProgressDownloads:(int)downloadCount { 518 NSString* warningText = nil; 519 NSString* explanationText = nil; 520 NSString* waitTitle = nil; 521 NSString* exitTitle = nil; 522 523 string16 product_name = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); 524 525 // Set the dialog text based on whether or not there are multiple downloads. 526 if (downloadCount == 1) { 527 // Dialog text: warning and explanation. 528 warningText = l10n_util::GetNSStringF( 529 IDS_SINGLE_DOWNLOAD_REMOVE_CONFIRM_WARNING, product_name); 530 explanationText = l10n_util::GetNSStringF( 531 IDS_SINGLE_DOWNLOAD_REMOVE_CONFIRM_EXPLANATION, product_name); 532 533 // Cancel download and exit button text. 534 exitTitle = l10n_util::GetNSString( 535 IDS_SINGLE_DOWNLOAD_REMOVE_CONFIRM_OK_BUTTON_LABEL); 536 537 // Wait for download button text. 538 waitTitle = l10n_util::GetNSString( 539 IDS_SINGLE_DOWNLOAD_REMOVE_CONFIRM_CANCEL_BUTTON_LABEL); 540 } else { 541 // Dialog text: warning and explanation. 542 warningText = l10n_util::GetNSStringF( 543 IDS_MULTIPLE_DOWNLOADS_REMOVE_CONFIRM_WARNING, product_name, 544 base::IntToString16(downloadCount)); 545 explanationText = l10n_util::GetNSStringF( 546 IDS_MULTIPLE_DOWNLOADS_REMOVE_CONFIRM_EXPLANATION, product_name); 547 548 // Cancel downloads and exit button text. 549 exitTitle = l10n_util::GetNSString( 550 IDS_MULTIPLE_DOWNLOADS_REMOVE_CONFIRM_OK_BUTTON_LABEL); 551 552 // Wait for downloads button text. 553 waitTitle = l10n_util::GetNSString( 554 IDS_MULTIPLE_DOWNLOADS_REMOVE_CONFIRM_CANCEL_BUTTON_LABEL); 555 } 556 557 // 'waitButton' is the default choice. 558 int choice = NSRunAlertPanel(warningText, explanationText, 559 waitTitle, exitTitle, nil); 560 return choice == NSAlertDefaultReturn ? YES : NO; 561} 562 563// Check all profiles for in progress downloads, and if we find any, prompt the 564// user to see if we should continue to exit (and thus cancel the downloads), or 565// if we should wait. 566- (BOOL)shouldQuitWithInProgressDownloads { 567 ProfileManager* profile_manager = g_browser_process->profile_manager(); 568 if (!profile_manager) 569 return YES; 570 571 ProfileManager::const_iterator it = profile_manager->begin(); 572 for (; it != profile_manager->end(); ++it) { 573 Profile* profile = *it; 574 DownloadManager* download_manager = profile->GetDownloadManager(); 575 if (download_manager && download_manager->in_progress_count() > 0) { 576 int downloadCount = download_manager->in_progress_count(); 577 if ([self userWillWaitForInProgressDownloads:downloadCount]) { 578 // Create a new browser window (if necessary) and navigate to the 579 // downloads page if the user chooses to wait. 580 Browser* browser = BrowserList::FindBrowserWithProfile(profile); 581 if (!browser) { 582 browser = Browser::Create(profile); 583 browser->window()->Show(); 584 } 585 DCHECK(browser); 586 browser->ShowDownloadsTab(); 587 return NO; 588 } 589 590 // User wants to exit. 591 return YES; 592 } 593 } 594 595 // No profiles or active downloads found, okay to exit. 596 return YES; 597} 598 599// Called to determine if we should enable the "restore tab" menu item. 600// Checks with the TabRestoreService to see if there's anything there to 601// restore and returns YES if so. 602- (BOOL)canRestoreTab { 603 TabRestoreService* service = [self defaultProfile]->GetTabRestoreService(); 604 return service && !service->entries().empty(); 605} 606 607// Returns true if there is not a modal window (either window- or application- 608// modal) blocking the active browser. Note that tab modal dialogs (HTTP auth 609// sheets) will not count as blocking the browser. But things like open/save 610// dialogs that are window modal will block the browser. 611- (BOOL)keyWindowIsNotModal { 612 Browser* browser = BrowserList::GetLastActive(); 613 return [NSApp modalWindow] == nil && (!browser || 614 ![[browser->window()->GetNativeHandle() attachedSheet] 615 isKindOfClass:[NSWindow class]]); 616} 617 618// Called to validate menu items when there are no key windows. All the 619// items we care about have been set with the |commandDispatch:| action and 620// a target of FirstResponder in IB. If it's not one of those, let it 621// continue up the responder chain to be handled elsewhere. We pull out the 622// tag as the cross-platform constant to differentiate and dispatch the 623// various commands. 624- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { 625 SEL action = [item action]; 626 BOOL enable = NO; 627 if (action == @selector(commandDispatch:)) { 628 NSInteger tag = [item tag]; 629 if (menuState_->SupportsCommand(tag)) { 630 switch (tag) { 631 // The File Menu commands are not automatically disabled by Cocoa when a 632 // dialog sheet obscures the browser window, so we disable several of 633 // them here. We don't need to include IDC_CLOSE_WINDOW, because 634 // app_controller is only activated when there are no key windows (see 635 // function comment). 636 case IDC_RESTORE_TAB: 637 enable = [self keyWindowIsNotModal] && [self canRestoreTab]; 638 break; 639 // Browser-level items that open in new tabs should not open if there's 640 // a window- or app-modal dialog. 641 case IDC_OPEN_FILE: 642 case IDC_NEW_TAB: 643 case IDC_SHOW_HISTORY: 644 case IDC_SHOW_BOOKMARK_MANAGER: 645 enable = [self keyWindowIsNotModal]; 646 break; 647 // Browser-level items that open in new windows. 648 case IDC_NEW_WINDOW: 649 case IDC_TASK_MANAGER: 650 // Allow the user to open a new window if there's a window-modal 651 // dialog. 652 enable = [self keyWindowIsNotModal] || ([NSApp modalWindow] == nil); 653 break; 654 case IDC_SYNC_BOOKMARKS: { 655 Profile* defaultProfile = [self defaultProfile]; 656 // The profile may be NULL during shutdown -- see 657 // http://code.google.com/p/chromium/issues/detail?id=43048 . 658 // 659 // TODO(akalin,viettrungluu): Figure out whether this method 660 // can be prevented from being called if defaultProfile is 661 // NULL. 662 if (!defaultProfile) { 663 LOG(WARNING) 664 << "NULL defaultProfile detected -- not doing anything"; 665 break; 666 } 667 enable = defaultProfile->IsSyncAccessible() && 668 [self keyWindowIsNotModal]; 669 sync_ui_util::UpdateSyncItem(item, enable, defaultProfile); 670 break; 671 } 672 default: 673 enable = menuState_->IsCommandEnabled(tag) ? 674 [self keyWindowIsNotModal] : NO; 675 } 676 } 677 } else if (action == @selector(terminate:)) { 678 enable = YES; 679 } else if (action == @selector(showPreferences:)) { 680 enable = YES; 681 } else if (action == @selector(orderFrontStandardAboutPanel:)) { 682 enable = YES; 683 } else if (action == @selector(newWindowFromDock:)) { 684 enable = YES; 685 } 686 return enable; 687} 688 689// Called when the user picks a menu item when there are no key windows, or when 690// there is no foreground browser window. Calls through to the browser object to 691// execute the command. This assumes that the command is supported and doesn't 692// check, otherwise it should have been disabled in the UI in 693// |-validateUserInterfaceItem:|. 694- (void)commandDispatch:(id)sender { 695 Profile* defaultProfile = [self defaultProfile]; 696 697 // Handle the case where we're dispatching a command from a sender that's in a 698 // browser window. This means that the command came from a background window 699 // and is getting here because the foreground window is not a browser window. 700 if ([sender respondsToSelector:@selector(window)]) { 701 id delegate = [[sender window] windowController]; 702 if ([delegate isKindOfClass:[BrowserWindowController class]]) { 703 [delegate commandDispatch:sender]; 704 return; 705 } 706 } 707 708 NSInteger tag = [sender tag]; 709 switch (tag) { 710 case IDC_NEW_TAB: 711 // Create a new tab in an existing browser window (which we activate) if 712 // possible. 713 if (Browser* browser = ActivateBrowser(defaultProfile)) { 714 browser->ExecuteCommand(IDC_NEW_TAB); 715 break; 716 } 717 // Else fall through to create new window. 718 case IDC_NEW_WINDOW: 719 CreateBrowser(defaultProfile); 720 break; 721 case IDC_FOCUS_LOCATION: 722 ActivateOrCreateBrowser(defaultProfile)-> 723 ExecuteCommand(IDC_FOCUS_LOCATION); 724 break; 725 case IDC_FOCUS_SEARCH: 726 ActivateOrCreateBrowser(defaultProfile)->ExecuteCommand(IDC_FOCUS_SEARCH); 727 break; 728 case IDC_NEW_INCOGNITO_WINDOW: 729 Browser::OpenEmptyWindow(defaultProfile->GetOffTheRecordProfile()); 730 break; 731 case IDC_RESTORE_TAB: 732 Browser::OpenWindowWithRestoredTabs(defaultProfile); 733 break; 734 case IDC_OPEN_FILE: 735 CreateBrowser(defaultProfile)->ExecuteCommand(IDC_OPEN_FILE); 736 break; 737 case IDC_CLEAR_BROWSING_DATA: { 738 // There may not be a browser open, so use the default profile. 739 [ClearBrowsingDataController 740 showClearBrowsingDialogForProfile:defaultProfile]; 741 break; 742 } 743 case IDC_IMPORT_SETTINGS: { 744 UserMetrics::RecordAction(UserMetricsAction("Import_ShowDlg"), 745 defaultProfile); 746 [ImportSettingsDialogController 747 showImportSettingsDialogForProfile:defaultProfile]; 748 break; 749 } 750 case IDC_SHOW_BOOKMARK_MANAGER: 751 UserMetrics::RecordAction(UserMetricsAction("ShowBookmarkManager"), 752 defaultProfile); 753 if (Browser* browser = ActivateBrowser(defaultProfile)) { 754 // Open a bookmark manager tab. 755 browser->OpenBookmarkManager(); 756 } else { 757 // No browser window, so create one for the bookmark manager tab. 758 Browser::OpenBookmarkManagerWindow(defaultProfile); 759 } 760 break; 761 case IDC_SHOW_HISTORY: 762 if (Browser* browser = ActivateBrowser(defaultProfile)) 763 browser->ShowHistoryTab(); 764 else 765 Browser::OpenHistoryWindow(defaultProfile); 766 break; 767 case IDC_SHOW_DOWNLOADS: 768 if (Browser* browser = ActivateBrowser(defaultProfile)) 769 browser->ShowDownloadsTab(); 770 else 771 Browser::OpenDownloadsWindow(defaultProfile); 772 break; 773 case IDC_MANAGE_EXTENSIONS: 774 if (Browser* browser = ActivateBrowser(defaultProfile)) 775 browser->ShowExtensionsTab(); 776 else 777 Browser::OpenExtensionsWindow(defaultProfile); 778 break; 779 case IDC_HELP_PAGE: 780 if (Browser* browser = ActivateBrowser(defaultProfile)) 781 browser->OpenHelpTab(); 782 else 783 Browser::OpenHelpWindow(defaultProfile); 784 break; 785 case IDC_REPORT_BUG: { 786 Browser* browser = BrowserList::GetLastActive(); 787 TabContents* currentTab = 788 browser ? browser->GetSelectedTabContents() : NULL; 789 BugReportWindowController* controller = 790 [[BugReportWindowController alloc] 791 initWithTabContents:currentTab 792 profile:[self defaultProfile]]; 793 [controller runModalDialog]; 794 break; 795 } 796 case IDC_SYNC_BOOKMARKS: 797 // The profile may be NULL during shutdown -- see 798 // http://code.google.com/p/chromium/issues/detail?id=43048 . 799 // 800 // TODO(akalin,viettrungluu): Figure out whether this method can 801 // be prevented from being called if defaultProfile is NULL. 802 if (!defaultProfile) { 803 LOG(WARNING) << "NULL defaultProfile detected -- not doing anything"; 804 break; 805 } 806 // TODO(akalin): Add a constant to denote starting sync from the 807 // main menu and use that instead of START_FROM_WRENCH. 808 sync_ui_util::OpenSyncMyBookmarksDialog( 809 defaultProfile, ProfileSyncService::START_FROM_WRENCH); 810 break; 811 case IDC_TASK_MANAGER: 812 UserMetrics::RecordAction(UserMetricsAction("TaskManager"), 813 defaultProfile); 814 TaskManagerMac::Show(); 815 break; 816 case IDC_OPTIONS: 817 [self showPreferences:sender]; 818 break; 819 } 820} 821 822// Same as |-commandDispatch:|, but executes commands using a disposition 823// determined by the key flags. This will get called in the case where the 824// frontmost window is not a browser window, and the user has command-clicked 825// a button in a background browser window whose action is 826// |-commandDispatchUsingKeyModifiers:| 827- (void)commandDispatchUsingKeyModifiers:(id)sender { 828 DCHECK(sender); 829 if ([sender respondsToSelector:@selector(window)]) { 830 id delegate = [[sender window] windowController]; 831 if ([delegate isKindOfClass:[BrowserWindowController class]]) { 832 [delegate commandDispatchUsingKeyModifiers:sender]; 833 } 834 } 835} 836 837// NSApplication delegate method called when someone clicks on the 838// dock icon and there are no open windows. To match standard mac 839// behavior, we should open a new window. 840- (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication 841 hasVisibleWindows:(BOOL)flag { 842 // If the browser is currently trying to quit, don't do anything and return NO 843 // to prevent AppKit from doing anything. 844 // TODO(rohitrao): Remove this code when http://crbug.com/40861 is resolved. 845 if (browser_shutdown::IsTryingToQuit()) 846 return NO; 847 848 // Don't do anything if there are visible windows. This will cause 849 // AppKit to unminimize the most recently minimized window. 850 if (flag) 851 return YES; 852 853 // If launched as a hidden login item (due to installation of a persistent app 854 // or by the user, for example in System Preferenecs->Accounts->Login Items), 855 // allow session to be restored first time the user clicks on a Dock icon. 856 // Normally, it'd just open a new empty page. 857 { 858 static BOOL doneOnce = NO; 859 if (!doneOnce) { 860 doneOnce = YES; 861 if (mac_util::WasLaunchedAsHiddenLoginItem()) { 862 SessionService* sessionService = 863 [self defaultProfile]->GetSessionService(); 864 if (sessionService && 865 sessionService->RestoreIfNecessary(std::vector<GURL>())) 866 return NO; 867 } 868 } 869 } 870 // Otherwise open a new window. 871 { 872 AutoReset<bool> auto_reset_in_run(&g_is_opening_new_window, true); 873 Browser::OpenEmptyWindow([self defaultProfile]); 874 } 875 876 // We've handled the reopen event, so return NO to tell AppKit not 877 // to do anything. 878 return NO; 879} 880 881- (void)initMenuState { 882 menuState_.reset(new CommandUpdater(NULL)); 883 menuState_->UpdateCommandEnabled(IDC_NEW_TAB, true); 884 menuState_->UpdateCommandEnabled(IDC_NEW_WINDOW, true); 885 menuState_->UpdateCommandEnabled(IDC_NEW_INCOGNITO_WINDOW, true); 886 menuState_->UpdateCommandEnabled(IDC_OPEN_FILE, true); 887 menuState_->UpdateCommandEnabled(IDC_CLEAR_BROWSING_DATA, true); 888 menuState_->UpdateCommandEnabled(IDC_RESTORE_TAB, false); 889 menuState_->UpdateCommandEnabled(IDC_FOCUS_LOCATION, true); 890 menuState_->UpdateCommandEnabled(IDC_FOCUS_SEARCH, true); 891 menuState_->UpdateCommandEnabled(IDC_SHOW_BOOKMARK_MANAGER, true); 892 menuState_->UpdateCommandEnabled(IDC_SHOW_HISTORY, true); 893 menuState_->UpdateCommandEnabled(IDC_SHOW_DOWNLOADS, true); 894 menuState_->UpdateCommandEnabled(IDC_MANAGE_EXTENSIONS, true); 895 menuState_->UpdateCommandEnabled(IDC_HELP_PAGE, true); 896 menuState_->UpdateCommandEnabled(IDC_IMPORT_SETTINGS, true); 897 menuState_->UpdateCommandEnabled(IDC_REPORT_BUG, true); 898 menuState_->UpdateCommandEnabled(IDC_SYNC_BOOKMARKS, true); 899 menuState_->UpdateCommandEnabled(IDC_TASK_MANAGER, true); 900} 901 902- (void)registerServicesMenuTypesTo:(NSApplication*)app { 903 // Note that RenderWidgetHostViewCocoa implements NSServicesRequests which 904 // handles requests from services. 905 NSArray* types = [NSArray arrayWithObjects:NSStringPboardType, nil]; 906 [app registerServicesMenuSendTypes:types returnTypes:types]; 907} 908 909- (Profile*)defaultProfile { 910 // TODO(jrg): Find a better way to get the "default" profile. 911 if (g_browser_process->profile_manager()) 912 return *g_browser_process->profile_manager()->begin(); 913 914 return NULL; 915} 916 917// Various methods to open URLs that we get in a native fashion. We use 918// BrowserInit here because on the other platforms, URLs to open come through 919// the ProcessSingleton, and it calls BrowserInit. It's best to bottleneck the 920// openings through that for uniform handling. 921 922- (void)openUrls:(const std::vector<GURL>&)urls { 923 // If the browser hasn't started yet, just queue up the URLs. 924 if (!startupComplete_) { 925 startupUrls_.insert(startupUrls_.end(), urls.begin(), urls.end()); 926 return; 927 } 928 929 Browser* browser = BrowserList::GetLastActive(); 930 // if no browser window exists then create one with no tabs to be filled in 931 if (!browser) { 932 browser = Browser::Create([self defaultProfile]); 933 browser->window()->Show(); 934 } 935 936 CommandLine dummy(CommandLine::ARGUMENTS_ONLY); 937 BrowserInit::LaunchWithProfile launch(FilePath(), dummy); 938 launch.OpenURLsInBrowser(browser, false, urls); 939} 940 941- (void)getUrl:(NSAppleEventDescriptor*)event 942 withReply:(NSAppleEventDescriptor*)reply { 943 NSString* urlStr = [[event paramDescriptorForKeyword:keyDirectObject] 944 stringValue]; 945 946 GURL gurl(base::SysNSStringToUTF8(urlStr)); 947 std::vector<GURL> gurlVector; 948 gurlVector.push_back(gurl); 949 950 [self openUrls:gurlVector]; 951} 952 953- (void)application:(NSApplication*)sender 954 openFiles:(NSArray*)filenames { 955 std::vector<GURL> gurlVector; 956 for (NSString* file in filenames) { 957 GURL gurl = net::FilePathToFileURL(FilePath(base::SysNSStringToUTF8(file))); 958 gurlVector.push_back(gurl); 959 } 960 if (!gurlVector.empty()) 961 [self openUrls:gurlVector]; 962 else 963 NOTREACHED() << "Nothing to open!"; 964 965 [sender replyToOpenOrPrint:NSApplicationDelegateReplySuccess]; 966} 967 968// Called when the preferences window is closed. We use this to release the 969// window controller. 970- (void)prefsWindowClosed:(NSNotification*)notification { 971 NSWindow* window = [prefsController_ window]; 972 DCHECK([notification object] == window); 973 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; 974 [defaultCenter removeObserver:self 975 name:NSWindowWillCloseNotification 976 object:window]; 977 // PreferencesWindowControllers are autoreleased in 978 // -[PreferencesWindowController windowWillClose:]. 979 prefsController_ = nil; 980} 981 982// Show the preferences window, or bring it to the front if it's already 983// visible. 984- (IBAction)showPreferences:(id)sender { 985 const CommandLine& parsed_command_line = *CommandLine::ForCurrentProcess(); 986 if (parsed_command_line.HasSwitch(switches::kEnableTabbedOptions)) { 987 if (Browser* browser = ActivateBrowser([self defaultProfile])) { 988 // Show options tab in the active browser window. 989 browser->ShowOptionsTab(chrome::kDefaultOptionsSubPage); 990 } else { 991 // No browser window, so create one for the options tab. 992 Browser::OpenOptionsWindow([self defaultProfile]); 993 } 994 } else { 995 [self showPreferencesWindow:sender 996 page:OPTIONS_PAGE_DEFAULT 997 profile:[self defaultProfile]]; 998 } 999} 1000 1001- (void)showPreferencesWindow:(id)sender 1002 page:(OptionsPage)page 1003 profile:(Profile*)profile { 1004 if (prefsController_) { 1005 [prefsController_ switchToPage:page animate:YES]; 1006 } else { 1007 prefsController_ = 1008 [[PreferencesWindowController alloc] initWithProfile:profile 1009 initialPage:page]; 1010 // Watch for a notification of when it goes away so that we can destroy 1011 // the controller. 1012 [[NSNotificationCenter defaultCenter] 1013 addObserver:self 1014 selector:@selector(prefsWindowClosed:) 1015 name:NSWindowWillCloseNotification 1016 object:[prefsController_ window]]; 1017 } 1018 [prefsController_ showPreferences:sender]; 1019} 1020 1021// Called when the about window is closed. We use this to release the 1022// window controller. 1023- (void)aboutWindowClosed:(NSNotification*)notification { 1024 NSWindow* window = [aboutController_ window]; 1025 DCHECK(window == [notification object]); 1026 [[NSNotificationCenter defaultCenter] 1027 removeObserver:self 1028 name:NSWindowWillCloseNotification 1029 object:window]; 1030 // AboutWindowControllers are autoreleased in 1031 // -[AboutWindowController windowWillClose:]. 1032 aboutController_ = nil; 1033} 1034 1035- (IBAction)orderFrontStandardAboutPanel:(id)sender { 1036 if (!aboutController_) { 1037 aboutController_ = 1038 [[AboutWindowController alloc] initWithProfile:[self defaultProfile]]; 1039 1040 // Watch for a notification of when it goes away so that we can destroy 1041 // the controller. 1042 [[NSNotificationCenter defaultCenter] 1043 addObserver:self 1044 selector:@selector(aboutWindowClosed:) 1045 name:NSWindowWillCloseNotification 1046 object:[aboutController_ window]]; 1047 } 1048 1049 [aboutController_ showWindow:self]; 1050} 1051 1052// Explicitly bring to the foreground when creating new windows from the dock. 1053- (void)newWindowFromDock:(id)sender { 1054 [NSApp activateIgnoringOtherApps:YES]; 1055 [self commandDispatch:sender]; 1056} 1057 1058- (NSMenu*)applicationDockMenu:(NSApplication*)sender { 1059 NSMenu* dockMenu = [[[NSMenu alloc] initWithTitle: @""] autorelease]; 1060 NSString* titleStr = l10n_util::GetNSStringWithFixup(IDS_NEW_WINDOW_MAC); 1061 scoped_nsobject<NSMenuItem> item([[NSMenuItem alloc] 1062 initWithTitle:titleStr 1063 action:@selector(newWindowFromDock:) 1064 keyEquivalent:@""]); 1065 [item setTarget:self]; 1066 [item setTag:IDC_NEW_WINDOW]; 1067 [dockMenu addItem:item]; 1068 1069 titleStr = l10n_util::GetNSStringWithFixup(IDS_NEW_INCOGNITO_WINDOW_MAC); 1070 item.reset([[NSMenuItem alloc] initWithTitle:titleStr 1071 action:@selector(newWindowFromDock:) 1072 keyEquivalent:@""]); 1073 [item setTarget:self]; 1074 [item setTag:IDC_NEW_INCOGNITO_WINDOW]; 1075 [dockMenu addItem:item]; 1076 1077 return dockMenu; 1078} 1079 1080- (const std::vector<GURL>&)startupUrls { 1081 return startupUrls_; 1082} 1083 1084- (void)clearStartupUrls { 1085 startupUrls_.clear(); 1086} 1087 1088@end // @implementation AppController 1089 1090//--------------------------------------------------------------------------- 1091 1092void ShowOptionsWindow(OptionsPage page, 1093 OptionsGroup highlight_group, 1094 Profile* profile) { 1095 // TODO(akalin): Use highlight_group. 1096 AppController* appController = [NSApp delegate]; 1097 [appController showPreferencesWindow:nil page:page profile:profile]; 1098} 1099 1100namespace app_controller_mac { 1101 1102bool IsOpeningNewWindow() { 1103 return g_is_opening_new_window; 1104} 1105 1106} // namespace app_controller_mac 1107