1// Copyright (c) 2012 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 "base/auto_reset.h"
8#include "base/bind.h"
9#include "base/command_line.h"
10#include "base/files/file_path.h"
11#include "base/mac/foundation_util.h"
12#include "base/mac/mac_util.h"
13#include "base/mac/sdk_forward_declarations.h"
14#include "base/message_loop/message_loop.h"
15#include "base/prefs/pref_service.h"
16#include "base/strings/string_number_conversions.h"
17#include "base/strings/sys_string_conversions.h"
18#include "base/strings/utf_string_conversions.h"
19#include "chrome/app/chrome_command_ids.h"
20#include "chrome/browser/apps/app_shim/extension_app_shim_handler_mac.h"
21#include "chrome/browser/apps/app_window_registry_util.h"
22#include "chrome/browser/background/background_application_list_model.h"
23#include "chrome/browser/background/background_mode_manager.h"
24#include "chrome/browser/browser_process.h"
25#include "chrome/browser/browser_shutdown.h"
26#include "chrome/browser/chrome_notification_types.h"
27#include "chrome/browser/command_updater.h"
28#include "chrome/browser/download/download_service.h"
29#include "chrome/browser/download/download_service_factory.h"
30#include "chrome/browser/extensions/extension_service.h"
31#include "chrome/browser/first_run/first_run.h"
32#include "chrome/browser/lifetime/application_lifetime.h"
33#include "chrome/browser/mac/mac_startup_profiler.h"
34#include "chrome/browser/profiles/profile_info_cache_observer.h"
35#include "chrome/browser/profiles/profile_manager.h"
36#include "chrome/browser/profiles/profiles_state.h"
37#include "chrome/browser/sessions/session_restore.h"
38#include "chrome/browser/sessions/session_service.h"
39#include "chrome/browser/sessions/session_service_factory.h"
40#include "chrome/browser/sessions/tab_restore_service.h"
41#include "chrome/browser/sessions/tab_restore_service_factory.h"
42#include "chrome/browser/signin/signin_manager_factory.h"
43#include "chrome/browser/signin/signin_promo.h"
44#include "chrome/browser/sync/profile_sync_service.h"
45#include "chrome/browser/sync/sync_ui_util.h"
46#include "chrome/browser/ui/browser.h"
47#include "chrome/browser/ui/browser_command_controller.h"
48#include "chrome/browser/ui/browser_commands.h"
49#include "chrome/browser/ui/browser_dialogs.h"
50#include "chrome/browser/ui/browser_finder.h"
51#include "chrome/browser/ui/browser_iterator.h"
52#include "chrome/browser/ui/browser_mac.h"
53#include "chrome/browser/ui/browser_window.h"
54#include "chrome/browser/ui/chrome_pages.h"
55#import "chrome/browser/ui/cocoa/apps/app_shim_menu_controller_mac.h"
56#include "chrome/browser/ui/cocoa/apps/quit_with_apps_controller_mac.h"
57#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
58#import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
59#import "chrome/browser/ui/cocoa/browser_window_controller.h"
60#import "chrome/browser/ui/cocoa/confirm_quit.h"
61#import "chrome/browser/ui/cocoa/confirm_quit_panel_controller.h"
62#import "chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h"
63#import "chrome/browser/ui/cocoa/history_menu_bridge.h"
64#include "chrome/browser/ui/cocoa/last_active_browser_cocoa.h"
65#import "chrome/browser/ui/cocoa/profiles/profile_menu_controller.h"
66#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
67#import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
68#include "chrome/browser/ui/cocoa/task_manager_mac.h"
69#include "chrome/browser/ui/extensions/application_launch.h"
70#include "chrome/browser/ui/host_desktop.h"
71#include "chrome/browser/ui/startup/startup_browser_creator.h"
72#include "chrome/browser/ui/startup/startup_browser_creator_impl.h"
73#include "chrome/browser/ui/user_manager.h"
74#include "chrome/common/chrome_paths_internal.h"
75#include "chrome/common/chrome_switches.h"
76#include "chrome/common/cloud_print/cloud_print_class_mac.h"
77#include "chrome/common/extensions/extension_constants.h"
78#include "chrome/common/mac/app_mode_common.h"
79#include "chrome/common/pref_names.h"
80#include "chrome/common/url_constants.h"
81#include "chrome/grit/chromium_strings.h"
82#include "chrome/grit/generated_resources.h"
83#include "components/signin/core/browser/signin_manager.h"
84#include "components/signin/core/common/profile_management_switches.h"
85#include "content/public/browser/browser_thread.h"
86#include "content/public/browser/download_manager.h"
87#include "content/public/browser/notification_service.h"
88#include "content/public/browser/notification_types.h"
89#include "content/public/browser/plugin_service.h"
90#include "content/public/browser/user_metrics.h"
91#include "extensions/browser/extension_system.h"
92#include "net/base/filename_util.h"
93#include "ui/base/cocoa/focus_window_set.h"
94#include "ui/base/l10n/l10n_util.h"
95#include "ui/base/l10n/l10n_util_mac.h"
96
97using base::UserMetricsAction;
98using content::BrowserContext;
99using content::BrowserThread;
100using content::DownloadManager;
101
102namespace {
103
104// Declare notification names from the 10.7 SDK.
105#if !defined(MAC_OS_X_VERSION_10_7) || \
106    MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
107NSString* NSPopoverDidShowNotification = @"NSPopoverDidShowNotification";
108NSString* NSPopoverDidCloseNotification = @"NSPopoverDidCloseNotification";
109#endif
110
111// How long we allow a workspace change notification to wait to be
112// associated with a dock activation. The animation lasts 250ms. See
113// applicationShouldHandleReopen:hasVisibleWindows:.
114static const int kWorkspaceChangeTimeoutMs = 500;
115
116// True while AppController is calling chrome::NewEmptyWindow(). We need a
117// global flag here, analogue to StartupBrowserCreator::InProcessStartup()
118// because otherwise the SessionService will try to restore sessions when we
119// make a new window while there are no other active windows.
120bool g_is_opening_new_window = false;
121
122// Activates a browser window having the given profile (the last one active) if
123// possible and returns a pointer to the activate |Browser| or NULL if this was
124// not possible. If the last active browser is minimized (in particular, if
125// there are only minimized windows), it will unminimize it.
126Browser* ActivateBrowser(Profile* profile) {
127  Browser* browser = chrome::FindLastActiveWithProfile(
128      profile->IsGuestSession() ? profile->GetOffTheRecordProfile() : profile,
129      chrome::HOST_DESKTOP_TYPE_NATIVE);
130  if (browser)
131    browser->window()->Activate();
132  return browser;
133}
134
135// Creates an empty browser window with the given profile and returns a pointer
136// to the new |Browser|.
137Browser* CreateBrowser(Profile* profile) {
138  {
139    base::AutoReset<bool> auto_reset_in_run(&g_is_opening_new_window, true);
140    chrome::NewEmptyWindow(profile, chrome::HOST_DESKTOP_TYPE_NATIVE);
141  }
142
143  Browser* browser = chrome::GetLastActiveBrowser();
144  CHECK(browser);
145  return browser;
146}
147
148// Activates a browser window having the given profile (the last one active) if
149// possible or creates an empty one if necessary. Returns a pointer to the
150// activated/new |Browser|.
151Browser* ActivateOrCreateBrowser(Profile* profile) {
152  if (Browser* browser = ActivateBrowser(profile))
153    return browser;
154  return CreateBrowser(profile);
155}
156
157CFStringRef BaseBundleID_CFString() {
158  NSString* base_bundle_id =
159      [NSString stringWithUTF8String:base::mac::BaseBundleID()];
160  return base::mac::NSToCFCast(base_bundle_id);
161}
162
163// This callback synchronizes preferences (under "org.chromium.Chromium" or
164// "com.google.Chrome"), in particular, writes them out to disk.
165void PrefsSyncCallback() {
166  if (!CFPreferencesAppSynchronize(BaseBundleID_CFString()))
167    LOG(WARNING) << "Error recording application bundle path.";
168}
169
170// Record the location of the application bundle (containing the main framework)
171// from which Chromium was loaded. This is used by app mode shims to find
172// Chromium.
173void RecordLastRunAppBundlePath() {
174  // Going up three levels from |chrome::GetVersionedDirectory()| gives the
175  // real, user-visible app bundle directory. (The alternatives give either the
176  // framework's path or the initial app's path, which may be an app mode shim
177  // or a unit test.)
178  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
179
180  base::FilePath app_bundle_path =
181      chrome::GetVersionedDirectory().DirName().DirName().DirName();
182  base::ScopedCFTypeRef<CFStringRef> app_bundle_path_cfstring(
183      base::SysUTF8ToCFStringRef(app_bundle_path.value()));
184  CFPreferencesSetAppValue(
185      base::mac::NSToCFCast(app_mode::kLastRunAppBundlePathPrefsKey),
186      app_bundle_path_cfstring, BaseBundleID_CFString());
187
188  // Sync after a delay avoid I/O contention on startup; 1500 ms is plenty.
189  BrowserThread::PostDelayedTask(
190      BrowserThread::FILE, FROM_HERE,
191      base::Bind(&PrefsSyncCallback),
192      base::TimeDelta::FromMilliseconds(1500));
193}
194
195bool IsProfileSignedOut(Profile* profile) {
196  // The signed out status only makes sense at the moment in the context of the
197  // --new-profile-management flag.
198  if (!switches::IsNewProfileManagement())
199    return false;
200  ProfileInfoCache& cache =
201      g_browser_process->profile_manager()->GetProfileInfoCache();
202  size_t profile_index = cache.GetIndexOfProfileWithPath(profile->GetPath());
203  if (profile_index == std::string::npos)
204    return false;
205  return cache.ProfileIsSigninRequiredAtIndex(profile_index);
206}
207
208}  // anonymous namespace
209
210@interface AppController (Private)
211- (void)initMenuState;
212- (void)initProfileMenu;
213- (void)updateConfirmToQuitPrefMenuItem:(NSMenuItem*)item;
214- (void)updateDisplayMessageCenterPrefMenuItem:(NSMenuItem*)item;
215- (void)registerServicesMenuTypesTo:(NSApplication*)app;
216- (void)openUrls:(const std::vector<GURL>&)urls;
217- (void)getUrl:(NSAppleEventDescriptor*)event
218     withReply:(NSAppleEventDescriptor*)reply;
219- (void)windowLayeringDidChange:(NSNotification*)inNotification;
220- (void)activeSpaceDidChange:(NSNotification*)inNotification;
221- (void)checkForAnyKeyWindows;
222- (BOOL)userWillWaitForInProgressDownloads:(int)downloadCount;
223- (BOOL)shouldQuitWithInProgressDownloads;
224- (void)executeApplication:(id)sender;
225- (void)profileWasRemoved:(const base::FilePath&)profilePath;
226@end
227
228class AppControllerProfileObserver : public ProfileInfoCacheObserver {
229 public:
230  AppControllerProfileObserver(
231      ProfileManager* profile_manager, AppController* app_controller)
232      : profile_manager_(profile_manager),
233        app_controller_(app_controller) {
234    DCHECK(profile_manager_);
235    DCHECK(app_controller_);
236    profile_manager_->GetProfileInfoCache().AddObserver(this);
237  }
238
239  virtual ~AppControllerProfileObserver() {
240    DCHECK(profile_manager_);
241    profile_manager_->GetProfileInfoCache().RemoveObserver(this);
242  }
243
244 private:
245  // ProfileInfoCacheObserver implementation:
246
247  virtual void OnProfileAdded(const base::FilePath& profile_path) OVERRIDE {
248  }
249
250  virtual void OnProfileWasRemoved(
251      const base::FilePath& profile_path,
252      const base::string16& profile_name) OVERRIDE {
253    // When a profile is deleted we need to notify the AppController,
254    // so it can correctly update its pointer to the last used profile.
255    [app_controller_ profileWasRemoved:profile_path];
256  }
257
258  virtual void OnProfileWillBeRemoved(
259      const base::FilePath& profile_path) OVERRIDE {
260  }
261
262  virtual void OnProfileNameChanged(
263      const base::FilePath& profile_path,
264      const base::string16& old_profile_name) OVERRIDE {
265  }
266
267  virtual void OnProfileAvatarChanged(
268      const base::FilePath& profile_path) OVERRIDE {
269  }
270
271  ProfileManager* profile_manager_;
272
273  AppController* app_controller_;  // Weak; owns us.
274
275  DISALLOW_COPY_AND_ASSIGN(AppControllerProfileObserver);
276};
277
278@implementation AppController
279
280@synthesize startupComplete = startupComplete_;
281
282// This method is called very early in application startup (ie, before
283// the profile is loaded or any preferences have been registered). Defer any
284// user-data initialization until -applicationDidFinishLaunching:.
285- (void)awakeFromNib {
286  MacStartupProfiler::GetInstance()->Profile(
287      MacStartupProfiler::AWAKE_FROM_NIB);
288  // We need to register the handlers early to catch events fired on launch.
289  NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
290  [em setEventHandler:self
291          andSelector:@selector(getUrl:withReply:)
292        forEventClass:kInternetEventClass
293           andEventID:kAEGetURL];
294  [em setEventHandler:self
295          andSelector:@selector(getUrl:withReply:)
296        forEventClass:'WWW!'    // A particularly ancient AppleEvent that dates
297           andEventID:'OURL'];  // back to the Spyglass days.
298
299  // Register for various window layering changes. We use these to update
300  // various UI elements (command-key equivalents, etc) when the frontmost
301  // window changes.
302  NSNotificationCenter* notificationCenter =
303      [NSNotificationCenter defaultCenter];
304  [notificationCenter
305      addObserver:self
306         selector:@selector(windowLayeringDidChange:)
307             name:NSWindowDidBecomeKeyNotification
308           object:nil];
309  [notificationCenter
310      addObserver:self
311         selector:@selector(windowLayeringDidChange:)
312             name:NSWindowDidResignKeyNotification
313           object:nil];
314  [notificationCenter
315      addObserver:self
316         selector:@selector(windowLayeringDidChange:)
317             name:NSWindowDidBecomeMainNotification
318           object:nil];
319  [notificationCenter
320      addObserver:self
321         selector:@selector(windowLayeringDidChange:)
322             name:NSWindowDidResignMainNotification
323           object:nil];
324
325  if (base::mac::IsOSLionOrLater()) {
326    [notificationCenter
327        addObserver:self
328           selector:@selector(popoverDidShow:)
329               name:NSPopoverDidShowNotification
330             object:nil];
331    [notificationCenter
332        addObserver:self
333           selector:@selector(popoverDidClose:)
334               name:NSPopoverDidCloseNotification
335             object:nil];
336  }
337
338  // Register for space change notifications.
339  [[[NSWorkspace sharedWorkspace] notificationCenter]
340    addObserver:self
341       selector:@selector(activeSpaceDidChange:)
342           name:NSWorkspaceActiveSpaceDidChangeNotification
343         object:nil];
344
345  // Set up the command updater for when there are no windows open
346  [self initMenuState];
347
348  // Initialize the Profile menu.
349  [self initProfileMenu];
350}
351
352- (void)unregisterEventHandlers {
353  NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
354  [em removeEventHandlerForEventClass:kInternetEventClass
355                           andEventID:kAEGetURL];
356  [em removeEventHandlerForEventClass:cloud_print::kAECloudPrintClass
357                           andEventID:cloud_print::kAECloudPrintClass];
358  [em removeEventHandlerForEventClass:'WWW!'
359                           andEventID:'OURL'];
360  [[NSNotificationCenter defaultCenter] removeObserver:self];
361  [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
362}
363
364// (NSApplicationDelegate protocol) This is the Apple-approved place to override
365// the default handlers.
366- (void)applicationWillFinishLaunching:(NSNotification*)notification {
367  MacStartupProfiler::GetInstance()->Profile(
368      MacStartupProfiler::WILL_FINISH_LAUNCHING);
369}
370
371- (void)applicationWillHide:(NSNotification*)notification {
372  apps::ExtensionAppShimHandler::OnChromeWillHide();
373}
374
375- (BOOL)tryToTerminateApplication:(NSApplication*)app {
376  // Check for in-process downloads, and prompt the user if they really want
377  // to quit (and thus cancel downloads). Only check if we're not already
378  // shutting down, else the user might be prompted multiple times if the
379  // download isn't stopped before terminate is called again.
380  if (!browser_shutdown::IsTryingToQuit() &&
381      ![self shouldQuitWithInProgressDownloads])
382    return NO;
383
384  // TODO(viettrungluu): Remove Apple Event handlers here? (It's safe to leave
385  // them in, but I'm not sure about UX; we'd also want to disable other things
386  // though.) http://crbug.com/40861
387
388  // Check if the user really wants to quit by employing the confirm-to-quit
389  // mechanism.
390  if (!browser_shutdown::IsTryingToQuit() &&
391      [self applicationShouldTerminate:app] != NSTerminateNow)
392    return NO;
393
394  // Check for active apps. If quitting is prevented, only close browsers and
395  // sessions.
396  if (!browser_shutdown::IsTryingToQuit() && quitWithAppsController_.get() &&
397      !quitWithAppsController_->ShouldQuit()) {
398    content::NotificationService::current()->Notify(
399        chrome::NOTIFICATION_CLOSE_ALL_BROWSERS_REQUEST,
400        content::NotificationService::AllSources(),
401        content::NotificationService::NoDetails());
402    // This will close all browser sessions.
403    chrome::CloseAllBrowsers();
404    return NO;
405  }
406
407  size_t num_browsers = chrome::GetTotalBrowserCount();
408
409  // Initiate a shutdown (via chrome::CloseAllBrowsersAndQuit()) if we aren't
410  // already shutting down.
411  if (!browser_shutdown::IsTryingToQuit()) {
412    content::NotificationService::current()->Notify(
413        chrome::NOTIFICATION_CLOSE_ALL_BROWSERS_REQUEST,
414        content::NotificationService::AllSources(),
415        content::NotificationService::NoDetails());
416    chrome::CloseAllBrowsersAndQuit();
417  }
418
419  return num_browsers == 0 ? YES : NO;
420}
421
422- (void)stopTryingToTerminateApplication:(NSApplication*)app {
423  if (browser_shutdown::IsTryingToQuit()) {
424    // Reset the "trying to quit" state, so that closing all browser windows
425    // will no longer lead to termination.
426    browser_shutdown::SetTryingToQuit(false);
427
428    // TODO(viettrungluu): Were we to remove Apple Event handlers above, we
429    // would have to reinstall them here. http://crbug.com/40861
430  }
431}
432
433- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)app {
434  // If there are no windows, quit immediately.
435  if (chrome::BrowserIterator().done() &&
436      !AppWindowRegistryUtil::IsAppWindowRegisteredInAnyProfile(0)) {
437    return NSTerminateNow;
438  }
439
440  // Check if the preference is turned on.
441  const PrefService* prefs = g_browser_process->local_state();
442  if (!prefs->GetBoolean(prefs::kConfirmToQuitEnabled)) {
443    confirm_quit::RecordHistogram(confirm_quit::kNoConfirm);
444    return NSTerminateNow;
445  }
446
447  // If the application is going to terminate as the result of a Cmd+Q
448  // invocation, use the special sauce to prevent accidental quitting.
449  // http://dev.chromium.org/developers/design-documents/confirm-to-quit-experiment
450
451  // This logic is only for keyboard-initiated quits.
452  if (![ConfirmQuitPanelController eventTriggersFeature:[app currentEvent]])
453    return NSTerminateNow;
454
455  return [[ConfirmQuitPanelController sharedController]
456      runModalLoopForApplication:app];
457}
458
459// Called when the app is shutting down. Clean-up as appropriate.
460- (void)applicationWillTerminate:(NSNotification*)aNotification {
461  // There better be no browser windows left at this point.
462  CHECK_EQ(0u, chrome::GetTotalBrowserCount());
463
464  // Tell BrowserList not to keep the browser process alive. Once all the
465  // browsers get dealloc'd, it will stop the RunLoop and fall back into main().
466  chrome::DecrementKeepAliveCount();
467
468  // Reset all pref watching, as this object outlives the prefs system.
469  profilePrefRegistrar_.reset();
470  localPrefRegistrar_.RemoveAll();
471
472  [self unregisterEventHandlers];
473
474  appShimMenuController_.reset();
475}
476
477- (void)didEndMainMessageLoop {
478  DCHECK_EQ(0u, chrome::GetBrowserCount([self lastProfile],
479                                        chrome::HOST_DESKTOP_TYPE_NATIVE));
480  if (!chrome::GetBrowserCount([self lastProfile],
481                               chrome::HOST_DESKTOP_TYPE_NATIVE)) {
482    // As we're shutting down, we need to nuke the TabRestoreService, which
483    // will start the shutdown of the NavigationControllers and allow for
484    // proper shutdown. If we don't do this, Chrome won't shut down cleanly,
485    // and may end up crashing when some thread tries to use the IO thread (or
486    // another thread) that is no longer valid.
487    TabRestoreServiceFactory::ResetForProfile([self lastProfile]);
488  }
489}
490
491// If the window has a tab controller, make "close window" be cmd-shift-w,
492// otherwise leave it as the normal cmd-w. Capitalization of the key equivalent
493// affects whether the shift modifier is used.
494- (void)adjustCloseWindowMenuItemKeyEquivalent:(BOOL)enableCloseTabShortcut {
495  [closeWindowMenuItem_ setKeyEquivalent:(enableCloseTabShortcut ? @"W" :
496                                                                   @"w")];
497  [closeWindowMenuItem_ setKeyEquivalentModifierMask:NSCommandKeyMask];
498}
499
500// If the window has a tab controller, make "close tab" take over cmd-w,
501// otherwise it shouldn't have any key-equivalent because it should be disabled.
502- (void)adjustCloseTabMenuItemKeyEquivalent:(BOOL)enableCloseTabShortcut {
503  if (enableCloseTabShortcut) {
504    [closeTabMenuItem_ setKeyEquivalent:@"w"];
505    [closeTabMenuItem_ setKeyEquivalentModifierMask:NSCommandKeyMask];
506  } else {
507    [closeTabMenuItem_ setKeyEquivalent:@""];
508    [closeTabMenuItem_ setKeyEquivalentModifierMask:0];
509  }
510}
511
512// Explicitly remove any command-key equivalents from the close tab/window
513// menus so that nothing can go haywire if we get a user action during pending
514// updates.
515- (void)clearCloseMenuItemKeyEquivalents {
516  [closeTabMenuItem_ setKeyEquivalent:@""];
517  [closeTabMenuItem_ setKeyEquivalentModifierMask:0];
518  [closeWindowMenuItem_ setKeyEquivalent:@""];
519  [closeWindowMenuItem_ setKeyEquivalentModifierMask:0];
520}
521
522// See if the focused window window has tabs, and adjust the key equivalents for
523// Close Tab/Close Window accordingly.
524- (void)fixCloseMenuItemKeyEquivalents {
525  fileMenuUpdatePending_ = NO;
526
527  NSWindow* window = [NSApp keyWindow];
528  NSWindow* mainWindow = [NSApp mainWindow];
529  if (!window || ([window parentWindow] == mainWindow)) {
530    // If the key window is a child of the main window (e.g. a bubble), the main
531    // window should be the one that handles the close menu item action.
532    // Also, there might be a small amount of time where there is no key window;
533    // in that case as well, just use our main browser window if there is one.
534    // You might think that we should just always use the main window, but the
535    // "About Chrome" window serves as a counterexample.
536    window = mainWindow;
537  }
538
539  BOOL hasTabs =
540      [[window windowController] isKindOfClass:[TabWindowController class]];
541  BOOL enableCloseTabShortcut = hasTabs && !hasPopover_;
542  [self adjustCloseWindowMenuItemKeyEquivalent:enableCloseTabShortcut];
543  [self adjustCloseTabMenuItemKeyEquivalent:enableCloseTabShortcut];
544}
545
546// Fix up the "close tab/close window" command-key equivalents. We do this
547// after a delay to ensure that window layer state has been set by the time
548// we do the enabling. This should only be called on the main thread, code that
549// calls this (even as a side-effect) from other threads needs to be fixed.
550- (void)delayedFixCloseMenuItemKeyEquivalents {
551  DCHECK([NSThread isMainThread]);
552  if (!fileMenuUpdatePending_) {
553    // The OS prefers keypresses to timers, so it's possible that a cmd-w
554    // can sneak in before this timer fires. In order to prevent that from
555    // having any bad consequences, just clear the keys combos altogether. They
556    // will be reset when the timer eventually fires.
557    if ([NSThread isMainThread]) {
558      fileMenuUpdatePending_ = YES;
559      [self clearCloseMenuItemKeyEquivalents];
560      [self performSelector:@selector(fixCloseMenuItemKeyEquivalents)
561                 withObject:nil
562                 afterDelay:0];
563    } else {
564      // This shouldn't be happening, but if it does, force it to the main
565      // thread to avoid dropping the update. Don't mess with
566      // |fileMenuUpdatePending_| as it's not expected to be threadsafe and
567      // there could be a race between the selector finishing and setting the
568      // flag.
569      [self
570          performSelectorOnMainThread:@selector(fixCloseMenuItemKeyEquivalents)
571                           withObject:nil
572                        waitUntilDone:NO];
573    }
574  }
575}
576
577// Called when we get a notification about the window layering changing to
578// update the UI based on the new main window.
579- (void)windowLayeringDidChange:(NSNotification*)notify {
580  [self delayedFixCloseMenuItemKeyEquivalents];
581
582  if ([notify name] == NSWindowDidResignKeyNotification) {
583    // If a window is closed, this notification is fired but |[NSApp keyWindow]|
584    // returns nil regardless of whether any suitable candidates for the key
585    // window remain. It seems that the new key window for the app is not set
586    // until after this notification is fired, so a check is performed after the
587    // run loop is allowed to spin.
588    [self performSelector:@selector(checkForAnyKeyWindows)
589               withObject:nil
590               afterDelay:0.0];
591  }
592
593  // If the window changed to a new BrowserWindowController, update the profile.
594  id windowController = [[notify object] windowController];
595  if (![windowController isKindOfClass:[BrowserWindowController class]])
596    return;
597
598  if ([notify name] == NSWindowDidBecomeMainNotification) {
599    // If the profile is incognito, use the original profile.
600    Profile* newProfile = [windowController profile]->GetOriginalProfile();
601    [self windowChangedToProfile:newProfile];
602  } else if (chrome::GetTotalBrowserCount() == 0) {
603    [self windowChangedToProfile:
604        g_browser_process->profile_manager()->GetLastUsedProfile()];
605  }
606}
607
608- (void)activeSpaceDidChange:(NSNotification*)notify {
609  if (reopenTime_.is_null() ||
610      ![NSApp isActive] ||
611      (base::TimeTicks::Now() - reopenTime_).InMilliseconds() >
612      kWorkspaceChangeTimeoutMs) {
613    return;
614  }
615
616  // The last applicationShouldHandleReopen:hasVisibleWindows: call
617  // happened during a space change. Now that the change has
618  // completed, raise browser windows.
619  reopenTime_ = base::TimeTicks();
620  std::set<NSWindow*> browserWindows;
621  for (chrome::BrowserIterator iter; !iter.done(); iter.Next()) {
622    Browser* browser = *iter;
623    browserWindows.insert(browser->window()->GetNativeWindow());
624  }
625  if (!browserWindows.empty()) {
626    ui::FocusWindowSetOnCurrentSpace(browserWindows);
627  }
628}
629
630// Called on Lion and later when a popover (e.g. dictionary) is shown.
631- (void)popoverDidShow:(NSNotification*)notify {
632  hasPopover_ = YES;
633  [self fixCloseMenuItemKeyEquivalents];
634}
635
636// Called on Lion and later when a popover (e.g. dictionary) is closed.
637- (void)popoverDidClose:(NSNotification*)notify {
638  hasPopover_ = NO;
639  [self fixCloseMenuItemKeyEquivalents];
640}
641
642- (void)checkForAnyKeyWindows {
643  if ([NSApp keyWindow])
644    return;
645
646  content::NotificationService::current()->Notify(
647      chrome::NOTIFICATION_NO_KEY_WINDOW,
648      content::NotificationService::AllSources(),
649      content::NotificationService::NoDetails());
650}
651
652// If the auto-update interval is not set, make it 5 hours.
653// Placed here for 2 reasons:
654// 1) Same spot as other Pref stuff
655// 2) Try and be friendly by keeping this after app launch
656- (void)setUpdateCheckInterval {
657#if defined(GOOGLE_CHROME_BUILD)
658  CFStringRef app = CFSTR("com.google.Keystone.Agent");
659  CFStringRef checkInterval = CFSTR("checkInterval");
660  CFPropertyListRef plist = CFPreferencesCopyAppValue(checkInterval, app);
661  if (!plist) {
662    const float fiveHoursInSeconds = 5.0 * 60.0 * 60.0;
663    NSNumber* value = [NSNumber numberWithFloat:fiveHoursInSeconds];
664    CFPreferencesSetAppValue(checkInterval, value, app);
665    CFPreferencesAppSynchronize(app);
666  }
667#endif
668}
669
670- (void)openStartupUrls {
671  // On Mac, the URLs are passed in via Cocoa, not command line. The Chrome
672  // NSApplication is created in MainMessageLoop, and then the shortcut urls
673  // are passed in via Apple events. At this point, the first browser is
674  // already loaded in PreMainMessageLoop. If we initialize NSApplication
675  // before PreMainMessageLoop to capture shortcut URL events, it may cause
676  // more problems because it relies on things created in PreMainMessageLoop
677  // and may break existing message loop design.
678  if (startupUrls_.empty())
679    return;
680
681  // If there's only 1 tab and the tab is NTP, close this NTP tab and open all
682  // startup urls in new tabs, because the omnibox will stay focused if we
683  // load url in NTP tab.
684  Browser* browser = chrome::GetLastActiveBrowser();
685  int startupIndex = TabStripModel::kNoTab;
686  content::WebContents* startupContent = NULL;
687
688  if (browser && browser->tab_strip_model()->count() == 1) {
689    startupIndex = browser->tab_strip_model()->active_index();
690    startupContent = browser->tab_strip_model()->GetActiveWebContents();
691  }
692
693  if (startupUrls_.size()) {
694    [self openUrls:startupUrls_];
695    startupUrls_.clear();
696  }
697
698  if (startupIndex != TabStripModel::kNoTab &&
699      startupContent->GetVisibleURL() == GURL(chrome::kChromeUINewTabURL)) {
700    browser->tab_strip_model()->CloseWebContentsAt(startupIndex,
701        TabStripModel::CLOSE_NONE);
702  }
703}
704
705// This is called after profiles have been loaded and preferences registered.
706// It is safe to access the default profile here.
707- (void)applicationDidFinishLaunching:(NSNotification*)notify {
708  MacStartupProfiler::GetInstance()->Profile(
709      MacStartupProfiler::DID_FINISH_LAUNCHING);
710  MacStartupProfiler::GetInstance()->RecordMetrics();
711
712  // Notify BrowserList to keep the application running so it doesn't go away
713  // when all the browser windows get closed.
714  chrome::IncrementKeepAliveCount();
715
716  [self setUpdateCheckInterval];
717
718  // Start managing the menu for app windows. This needs to be done here because
719  // main menu item titles are not yet initialized in awakeFromNib.
720  [self initAppShimMenuController];
721
722  // If enabled, keep Chrome alive when apps are open instead of quitting all
723  // apps.
724  quitWithAppsController_ = new QuitWithAppsController();
725
726  // Build up the encoding menu, the order of the items differs based on the
727  // current locale (see http://crbug.com/7647 for details).
728  // We need a valid g_browser_process to get the profile which is why we can't
729  // call this from awakeFromNib.
730  NSMenu* viewMenu = [[[NSApp mainMenu] itemWithTag:IDC_VIEW_MENU] submenu];
731  NSMenuItem* encodingMenuItem = [viewMenu itemWithTag:IDC_ENCODING_MENU];
732  NSMenu* encodingMenu = [encodingMenuItem submenu];
733  EncodingMenuControllerDelegate::BuildEncodingMenu([self lastProfile],
734                                                    encodingMenu);
735
736  // Instantiate the ProfileInfoCache observer so that we can get
737  // notified when a profile is deleted.
738  profileInfoCacheObserver_.reset(new AppControllerProfileObserver(
739      g_browser_process->profile_manager(), self));
740
741  // Since Chrome is localized to more languages than the OS, tell Cocoa which
742  // menu is the Help so it can add the search item to it.
743  [NSApp setHelpMenu:helpMenu_];
744
745  // Record the path to the (browser) app bundle; this is used by the app mode
746  // shim.  It has to be done in FILE thread because getting the path requires
747  // I/O.
748  BrowserThread::PostTask(
749      BrowserThread::FILE, FROM_HERE,
750      base::Bind(&RecordLastRunAppBundlePath));
751
752  // Makes "Services" menu items available.
753  [self registerServicesMenuTypesTo:[notify object]];
754
755  startupComplete_ = YES;
756
757  [self openStartupUrls];
758
759  PrefService* localState = g_browser_process->local_state();
760  if (localState) {
761    localPrefRegistrar_.Init(localState);
762    localPrefRegistrar_.Add(
763        prefs::kAllowFileSelectionDialogs,
764        base::Bind(&chrome::BrowserCommandController::UpdateOpenFileState,
765                   menuState_.get()));
766  }
767}
768
769// This is called after profiles have been loaded and preferences registered.
770// It is safe to access the default profile here.
771- (void)applicationDidBecomeActive:(NSNotification*)notify {
772  content::PluginService::GetInstance()->AppActivated();
773}
774
775// Helper function for populating and displaying the in progress downloads at
776// exit alert panel.
777- (BOOL)userWillWaitForInProgressDownloads:(int)downloadCount {
778  NSString* titleText = nil;
779  NSString* explanationText = nil;
780  NSString* waitTitle = nil;
781  NSString* exitTitle = nil;
782
783  // Set the dialog text based on whether or not there are multiple downloads.
784  if (downloadCount == 1) {
785    // Dialog text: warning and explanation.
786    titleText = l10n_util::GetNSString(
787        IDS_SINGLE_DOWNLOAD_REMOVE_CONFIRM_TITLE);
788    explanationText = l10n_util::GetNSString(
789        IDS_SINGLE_DOWNLOAD_REMOVE_CONFIRM_EXPLANATION);
790  } else {
791    // Dialog text: warning and explanation.
792    titleText = l10n_util::GetNSString(
793        IDS_MULTIPLE_DOWNLOADS_REMOVE_CONFIRM_TITLE);
794    explanationText = l10n_util::GetNSString(
795        IDS_MULTIPLE_DOWNLOADS_REMOVE_CONFIRM_EXPLANATION);
796  }
797  // Cancel download and exit button text.
798  exitTitle = l10n_util::GetNSString(
799      IDS_DOWNLOAD_REMOVE_CONFIRM_OK_BUTTON_LABEL);
800
801  // Wait for download button text.
802  waitTitle = l10n_util::GetNSString(
803      IDS_DOWNLOAD_REMOVE_CONFIRM_CANCEL_BUTTON_LABEL);
804
805  // 'waitButton' is the default choice.
806  int choice = NSRunAlertPanel(titleText, @"%@",
807                               waitTitle, exitTitle, nil, explanationText);
808  return choice == NSAlertDefaultReturn ? YES : NO;
809}
810
811// Check all profiles for in progress downloads, and if we find any, prompt the
812// user to see if we should continue to exit (and thus cancel the downloads), or
813// if we should wait.
814- (BOOL)shouldQuitWithInProgressDownloads {
815  ProfileManager* profile_manager = g_browser_process->profile_manager();
816  if (!profile_manager)
817    return YES;
818
819  std::vector<Profile*> profiles(profile_manager->GetLoadedProfiles());
820  for (size_t i = 0; i < profiles.size(); ++i) {
821    DownloadService* download_service =
822      DownloadServiceFactory::GetForBrowserContext(profiles[i]);
823    DownloadManager* download_manager =
824        (download_service->HasCreatedDownloadManager() ?
825         BrowserContext::GetDownloadManager(profiles[i]) : NULL);
826    if (download_manager &&
827        download_manager->NonMaliciousInProgressCount() > 0) {
828      int downloadCount = download_manager->NonMaliciousInProgressCount();
829      if ([self userWillWaitForInProgressDownloads:downloadCount]) {
830        // Create a new browser window (if necessary) and navigate to the
831        // downloads page if the user chooses to wait.
832        Browser* browser = chrome::FindBrowserWithProfile(
833            profiles[i], chrome::HOST_DESKTOP_TYPE_NATIVE);
834        if (!browser) {
835          browser = new Browser(Browser::CreateParams(
836              profiles[i], chrome::HOST_DESKTOP_TYPE_NATIVE));
837          browser->window()->Show();
838        }
839        DCHECK(browser);
840        chrome::ShowDownloads(browser);
841        return NO;
842      }
843
844      // User wants to exit.
845      return YES;
846    }
847  }
848
849  // No profiles or active downloads found, okay to exit.
850  return YES;
851}
852
853// Called to determine if we should enable the "restore tab" menu item.
854// Checks with the TabRestoreService to see if there's anything there to
855// restore and returns YES if so.
856- (BOOL)canRestoreTab {
857  TabRestoreService* service =
858      TabRestoreServiceFactory::GetForProfile([self lastProfile]);
859  return service && !service->entries().empty();
860}
861
862// Called from the AppControllerProfileObserver every time a profile is deleted.
863- (void)profileWasRemoved:(const base::FilePath&)profilePath {
864  Profile* lastProfile = [self lastProfile];
865
866  // If the lastProfile has been deleted, the profile manager has
867  // already loaded a new one, so the pointer needs to be updated;
868  // otherwise we will try to start up a browser window with a pointer
869  // to the old profile.
870  if (profilePath == lastProfile->GetPath())
871    lastProfile_ = g_browser_process->profile_manager()->GetLastUsedProfile();
872}
873
874// Returns true if there is a modal window (either window- or application-
875// modal) blocking the active browser. Note that tab modal dialogs (HTTP auth
876// sheets) will not count as blocking the browser. But things like open/save
877// dialogs that are window modal will block the browser.
878- (BOOL)keyWindowIsModal {
879  if ([NSApp modalWindow])
880    return YES;
881
882  Browser* browser = chrome::GetLastActiveBrowser();
883  return browser &&
884         [[browser->window()->GetNativeWindow() attachedSheet]
885             isKindOfClass:[NSWindow class]];
886}
887
888// Called to validate menu items when there are no key windows. All the
889// items we care about have been set with the |commandDispatch:| action and
890// a target of FirstResponder in IB. If it's not one of those, let it
891// continue up the responder chain to be handled elsewhere. We pull out the
892// tag as the cross-platform constant to differentiate and dispatch the
893// various commands.
894- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
895  SEL action = [item action];
896  BOOL enable = NO;
897  if (action == @selector(commandDispatch:) ||
898      action == @selector(commandFromDock:)) {
899    NSInteger tag = [item tag];
900    if (menuState_ &&  // NULL in tests.
901        menuState_->SupportsCommand(tag)) {
902      switch (tag) {
903        // The File Menu commands are not automatically disabled by Cocoa when a
904        // dialog sheet obscures the browser window, so we disable several of
905        // them here.  We don't need to include IDC_CLOSE_WINDOW, because
906        // app_controller is only activated when there are no key windows (see
907        // function comment).
908        case IDC_RESTORE_TAB:
909          enable = ![self keyWindowIsModal] && [self canRestoreTab];
910          break;
911        // Browser-level items that open in new tabs should not open if there's
912        // a window- or app-modal dialog.
913        case IDC_OPEN_FILE:
914        case IDC_NEW_TAB:
915        case IDC_SHOW_HISTORY:
916        case IDC_SHOW_BOOKMARK_MANAGER:
917          enable = ![self keyWindowIsModal];
918          break;
919        // Browser-level items that open in new windows.
920        case IDC_TASK_MANAGER:
921          // Allow the user to open a new window if there's a window-modal
922          // dialog.
923          enable = ![self keyWindowIsModal];
924          break;
925        case IDC_SHOW_SYNC_SETUP: {
926          Profile* lastProfile = [self lastProfile];
927          // The profile may be NULL during shutdown -- see
928          // http://code.google.com/p/chromium/issues/detail?id=43048 .
929          //
930          // TODO(akalin,viettrungluu): Figure out whether this method
931          // can be prevented from being called if lastProfile is
932          // NULL.
933          if (!lastProfile) {
934            LOG(WARNING)
935                << "NULL lastProfile detected -- not doing anything";
936            break;
937          }
938          SigninManager* signin = SigninManagerFactory::GetForProfile(
939              lastProfile->GetOriginalProfile());
940          enable = signin->IsSigninAllowed() &&
941              ![self keyWindowIsModal];
942          [BrowserWindowController updateSigninItem:item
943                                         shouldShow:enable
944                                     currentProfile:lastProfile];
945          break;
946        }
947#if defined(GOOGLE_CHROME_BUILD)
948        case IDC_FEEDBACK:
949          enable = NO;
950          break;
951#endif
952        default:
953          enable = menuState_->IsCommandEnabled(tag) ?
954                   ![self keyWindowIsModal] : NO;
955      }
956    }
957  } else if (action == @selector(terminate:)) {
958    enable = YES;
959  } else if (action == @selector(showPreferences:)) {
960    enable = YES;
961  } else if (action == @selector(orderFrontStandardAboutPanel:)) {
962    enable = YES;
963  } else if (action == @selector(commandFromDock:)) {
964    enable = YES;
965  } else if (action == @selector(toggleConfirmToQuit:)) {
966    [self updateConfirmToQuitPrefMenuItem:static_cast<NSMenuItem*>(item)];
967    enable = YES;
968  } else if (action == @selector(toggleDisplayMessageCenter:)) {
969    NSMenuItem* menuItem = static_cast<NSMenuItem*>(item);
970    [self updateDisplayMessageCenterPrefMenuItem:menuItem];
971    enable = YES;
972  } else if (action == @selector(executeApplication:)) {
973    enable = YES;
974  }
975  return enable;
976}
977
978// Called when the user picks a menu item when there are no key windows, or when
979// there is no foreground browser window. Calls through to the browser object to
980// execute the command. This assumes that the command is supported and doesn't
981// check, otherwise it should have been disabled in the UI in
982// |-validateUserInterfaceItem:|.
983- (void)commandDispatch:(id)sender {
984  Profile* lastProfile = [self safeLastProfileForNewWindows];
985
986  // Handle the case where we're dispatching a command from a sender that's in a
987  // browser window. This means that the command came from a background window
988  // and is getting here because the foreground window is not a browser window.
989  if ([sender respondsToSelector:@selector(window)]) {
990    id delegate = [[sender window] windowController];
991    if ([delegate isKindOfClass:[BrowserWindowController class]]) {
992      [delegate commandDispatch:sender];
993      return;
994    }
995  }
996
997  // Ignore commands during session restore's browser creation.  It uses a
998  // nested message loop and commands dispatched during this operation cause
999  // havoc.
1000  if (SessionRestore::IsRestoring(lastProfile) &&
1001      base::MessageLoop::current()->IsNested())
1002    return;
1003
1004  NSInteger tag = [sender tag];
1005
1006  // If there are no browser windows, and we are trying to open a browser
1007  // for a locked profile, we have to show the User Manager instead as the
1008  // locked profile needs authentication.
1009  if (IsProfileSignedOut(lastProfile)) {
1010    UserManager::Show(lastProfile->GetPath(),
1011                      profiles::USER_MANAGER_NO_TUTORIAL,
1012                      profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1013    return;
1014  }
1015
1016  switch (tag) {
1017    case IDC_NEW_TAB:
1018      // Create a new tab in an existing browser window (which we activate) if
1019      // possible.
1020      if (Browser* browser = ActivateBrowser(lastProfile)) {
1021        chrome::ExecuteCommand(browser, IDC_NEW_TAB);
1022        break;
1023      }
1024      // Else fall through to create new window.
1025    case IDC_NEW_WINDOW:
1026      CreateBrowser(lastProfile);
1027      break;
1028    case IDC_FOCUS_LOCATION:
1029      chrome::ExecuteCommand(ActivateOrCreateBrowser(lastProfile),
1030                             IDC_FOCUS_LOCATION);
1031      break;
1032    case IDC_FOCUS_SEARCH:
1033      chrome::ExecuteCommand(ActivateOrCreateBrowser(lastProfile),
1034                             IDC_FOCUS_SEARCH);
1035      break;
1036    case IDC_NEW_INCOGNITO_WINDOW:
1037      CreateBrowser(lastProfile->GetOffTheRecordProfile());
1038      break;
1039    case IDC_RESTORE_TAB:
1040      // There is only the native desktop on Mac.
1041      chrome::OpenWindowWithRestoredTabs(lastProfile,
1042                                         chrome::HOST_DESKTOP_TYPE_NATIVE);
1043      break;
1044    case IDC_OPEN_FILE:
1045      chrome::ExecuteCommand(CreateBrowser(lastProfile), IDC_OPEN_FILE);
1046      break;
1047    case IDC_CLEAR_BROWSING_DATA: {
1048      // There may not be a browser open, so use the default profile.
1049      if (Browser* browser = ActivateBrowser(lastProfile)) {
1050        chrome::ShowClearBrowsingDataDialog(browser);
1051      } else {
1052        chrome::OpenClearBrowsingDataDialogWindow(lastProfile);
1053      }
1054      break;
1055    }
1056    case IDC_IMPORT_SETTINGS: {
1057      if (Browser* browser = ActivateBrowser(lastProfile)) {
1058        chrome::ShowImportDialog(browser);
1059      } else {
1060        chrome::OpenImportSettingsDialogWindow(lastProfile);
1061      }
1062      break;
1063    }
1064    case IDC_SHOW_BOOKMARK_MANAGER:
1065      content::RecordAction(UserMetricsAction("ShowBookmarkManager"));
1066      if (Browser* browser = ActivateBrowser(lastProfile)) {
1067        chrome::ShowBookmarkManager(browser);
1068      } else {
1069        // No browser window, so create one for the bookmark manager tab.
1070        chrome::OpenBookmarkManagerWindow(lastProfile);
1071      }
1072      break;
1073    case IDC_SHOW_HISTORY:
1074      if (Browser* browser = ActivateBrowser(lastProfile))
1075        chrome::ShowHistory(browser);
1076      else
1077        chrome::OpenHistoryWindow(lastProfile);
1078      break;
1079    case IDC_SHOW_DOWNLOADS:
1080      if (Browser* browser = ActivateBrowser(lastProfile))
1081        chrome::ShowDownloads(browser);
1082      else
1083        chrome::OpenDownloadsWindow(lastProfile);
1084      break;
1085    case IDC_MANAGE_EXTENSIONS:
1086      if (Browser* browser = ActivateBrowser(lastProfile))
1087        chrome::ShowExtensions(browser, std::string());
1088      else
1089        chrome::OpenExtensionsWindow(lastProfile);
1090      break;
1091    case IDC_HELP_PAGE_VIA_MENU:
1092      if (Browser* browser = ActivateBrowser(lastProfile))
1093        chrome::ShowHelp(browser, chrome::HELP_SOURCE_MENU);
1094      else
1095        chrome::OpenHelpWindow(lastProfile, chrome::HELP_SOURCE_MENU);
1096      break;
1097    case IDC_SHOW_SYNC_SETUP:
1098      if (Browser* browser = ActivateBrowser(lastProfile)) {
1099        chrome::ShowBrowserSignin(browser, signin::SOURCE_MENU);
1100      } else {
1101        chrome::OpenSyncSetupWindow(lastProfile, signin::SOURCE_MENU);
1102      }
1103      break;
1104    case IDC_TASK_MANAGER:
1105      content::RecordAction(UserMetricsAction("TaskManager"));
1106      TaskManagerMac::Show();
1107      break;
1108    case IDC_OPTIONS:
1109      [self showPreferences:sender];
1110      break;
1111  }
1112}
1113
1114// Run a (background) application in a new tab.
1115- (void)executeApplication:(id)sender {
1116  NSInteger tag = [sender tag];
1117  Profile* profile = [self lastProfile];
1118  DCHECK(profile);
1119  BackgroundApplicationListModel applications(profile);
1120  DCHECK(tag >= 0 &&
1121         tag < static_cast<int>(applications.size()));
1122  const extensions::Extension* extension = applications.GetExtension(tag);
1123  BackgroundModeManager::LaunchBackgroundApplication(profile, extension);
1124}
1125
1126// Same as |-commandDispatch:|, but executes commands using a disposition
1127// determined by the key flags. This will get called in the case where the
1128// frontmost window is not a browser window, and the user has command-clicked
1129// a button in a background browser window whose action is
1130// |-commandDispatchUsingKeyModifiers:|
1131- (void)commandDispatchUsingKeyModifiers:(id)sender {
1132  DCHECK(sender);
1133  if ([sender respondsToSelector:@selector(window)]) {
1134    id delegate = [[sender window] windowController];
1135    if ([delegate isKindOfClass:[BrowserWindowController class]]) {
1136      [delegate commandDispatchUsingKeyModifiers:sender];
1137    }
1138  }
1139}
1140
1141// NSApplication delegate method called when someone clicks on the dock icon.
1142// To match standard mac behavior, we should open a new window if there are no
1143// browser windows.
1144- (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication
1145                    hasVisibleWindows:(BOOL)hasVisibleWindows {
1146  // If the browser is currently trying to quit, don't do anything and return NO
1147  // to prevent AppKit from doing anything.
1148  // TODO(rohitrao): Remove this code when http://crbug.com/40861 is resolved.
1149  if (browser_shutdown::IsTryingToQuit())
1150    return NO;
1151
1152  // Bring all browser windows to the front. Specifically, this brings them in
1153  // front of any app windows. FocusWindowSet will also unminimize the most
1154  // recently minimized window if no windows in the set are visible.
1155  // If there are any, return here. Otherwise, the windows are panels or
1156  // notifications so we still need to open a new window.
1157  if (hasVisibleWindows) {
1158    std::set<NSWindow*> browserWindows;
1159    for (chrome::BrowserIterator iter; !iter.done(); iter.Next()) {
1160      Browser* browser = *iter;
1161      browserWindows.insert(browser->window()->GetNativeWindow());
1162    }
1163    if (!browserWindows.empty()) {
1164      NSWindow* keyWindow = [NSApp keyWindow];
1165      if (keyWindow && ![keyWindow isOnActiveSpace]) {
1166        // The key window is not on the active space. We must be mid-animation
1167        // for a space transition triggered by the dock. Delay the call to
1168        // |ui::FocusWindowSet| until the transition completes. Otherwise, the
1169        // wrong space's windows get raised, resulting in an off-screen key
1170        // window. It does not work to |ui::FocusWindowSet| twice, once here
1171        // and once in |activeSpaceDidChange:|, as that appears to break when
1172        // the omnibox is focused.
1173        //
1174        // This check relies on OS X setting the key window to a window on the
1175        // target space before calling this method.
1176        //
1177        // See http://crbug.com/309656.
1178        reopenTime_ = base::TimeTicks::Now();
1179      } else {
1180        ui::FocusWindowSetOnCurrentSpace(browserWindows);
1181      }
1182      // Return NO; we've done (or soon will do) the deminiaturize, so
1183      // AppKit shouldn't do anything.
1184      return NO;
1185    }
1186  }
1187
1188  // If launched as a hidden login item (due to installation of a persistent app
1189  // or by the user, for example in System Preferences->Accounts->Login Items),
1190  // allow session to be restored first time the user clicks on a Dock icon.
1191  // Normally, it'd just open a new empty page.
1192  {
1193    static BOOL doneOnce = NO;
1194    BOOL attemptRestore = apps::AppShimHandler::ShouldRestoreSession() ||
1195        (!doneOnce && base::mac::WasLaunchedAsHiddenLoginItem());
1196    doneOnce = YES;
1197    if (attemptRestore) {
1198      SessionService* sessionService =
1199          SessionServiceFactory::GetForProfileForSessionRestore(
1200              [self lastProfile]);
1201      if (sessionService &&
1202          sessionService->RestoreIfNecessary(std::vector<GURL>()))
1203        return NO;
1204    }
1205  }
1206
1207  // Otherwise open a new window.
1208  // If the last profile was locked, we have to open the User Manager, as the
1209  // profile requires authentication. Similarly, because guest mode is
1210  // implemented as forced incognito, we can't open a new guest browser either,
1211  // so we have to show the User Manager as well.
1212  Profile* lastProfile = [self lastProfile];
1213  if (lastProfile->IsGuestSession() || IsProfileSignedOut(lastProfile)) {
1214    UserManager::Show(lastProfile->GetPath(),
1215                      profiles::USER_MANAGER_NO_TUTORIAL,
1216                      profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1217  } else {
1218    CreateBrowser(lastProfile);
1219  }
1220
1221  // We've handled the reopen event, so return NO to tell AppKit not
1222  // to do anything.
1223  return NO;
1224}
1225
1226- (void)initMenuState {
1227  menuState_.reset(new CommandUpdater(NULL));
1228  menuState_->UpdateCommandEnabled(IDC_NEW_TAB, true);
1229  menuState_->UpdateCommandEnabled(IDC_NEW_WINDOW, true);
1230  menuState_->UpdateCommandEnabled(IDC_NEW_INCOGNITO_WINDOW, true);
1231  menuState_->UpdateCommandEnabled(IDC_OPEN_FILE, true);
1232  menuState_->UpdateCommandEnabled(IDC_CLEAR_BROWSING_DATA, true);
1233  menuState_->UpdateCommandEnabled(IDC_RESTORE_TAB, false);
1234  menuState_->UpdateCommandEnabled(IDC_FOCUS_LOCATION, true);
1235  menuState_->UpdateCommandEnabled(IDC_FOCUS_SEARCH, true);
1236  menuState_->UpdateCommandEnabled(IDC_SHOW_BOOKMARK_MANAGER, true);
1237  menuState_->UpdateCommandEnabled(IDC_SHOW_HISTORY, true);
1238  menuState_->UpdateCommandEnabled(IDC_SHOW_DOWNLOADS, true);
1239  menuState_->UpdateCommandEnabled(IDC_MANAGE_EXTENSIONS, true);
1240  menuState_->UpdateCommandEnabled(IDC_HELP_PAGE_VIA_MENU, true);
1241  menuState_->UpdateCommandEnabled(IDC_IMPORT_SETTINGS, true);
1242#if defined(GOOGLE_CHROME_BUILD)
1243  menuState_->UpdateCommandEnabled(IDC_FEEDBACK, true);
1244#endif
1245  menuState_->UpdateCommandEnabled(IDC_SHOW_SYNC_SETUP, true);
1246  menuState_->UpdateCommandEnabled(IDC_TASK_MANAGER, true);
1247}
1248
1249// Conditionally adds the Profile menu to the main menu bar.
1250- (void)initProfileMenu {
1251  NSMenu* mainMenu = [NSApp mainMenu];
1252  NSMenuItem* profileMenu = [mainMenu itemWithTag:IDC_PROFILE_MAIN_MENU];
1253
1254  if (!profiles::IsMultipleProfilesEnabled()) {
1255    [mainMenu removeItem:profileMenu];
1256    return;
1257  }
1258
1259  // The controller will unhide the menu if necessary.
1260  [profileMenu setHidden:YES];
1261
1262  profileMenuController_.reset(
1263      [[ProfileMenuController alloc] initWithMainMenuItem:profileMenu]);
1264}
1265
1266// The Confirm to Quit preference is atypical in that the preference lives in
1267// the app menu right above the Quit menu item. This method will refresh the
1268// display of that item depending on the preference state.
1269- (void)updateConfirmToQuitPrefMenuItem:(NSMenuItem*)item {
1270  // Format the string so that the correct key equivalent is displayed.
1271  NSString* acceleratorString = [ConfirmQuitPanelController keyCommandString];
1272  NSString* title = l10n_util::GetNSStringF(IDS_CONFIRM_TO_QUIT_OPTION,
1273      base::SysNSStringToUTF16(acceleratorString));
1274  [item setTitle:title];
1275
1276  const PrefService* prefService = g_browser_process->local_state();
1277  bool enabled = prefService->GetBoolean(prefs::kConfirmToQuitEnabled);
1278  [item setState:enabled ? NSOnState : NSOffState];
1279}
1280
1281- (void)updateDisplayMessageCenterPrefMenuItem:(NSMenuItem*)item {
1282  const PrefService* prefService = g_browser_process->local_state();
1283  bool enabled = prefService->GetBoolean(prefs::kMessageCenterShowIcon);
1284  // The item should be checked if "show icon" is false, since the text reads
1285  // "Hide notification center icon."
1286  [item setState:enabled ? NSOffState : NSOnState];
1287}
1288
1289- (void)registerServicesMenuTypesTo:(NSApplication*)app {
1290  // Note that RenderWidgetHostViewCocoa implements NSServicesRequests which
1291  // handles requests from services.
1292  NSArray* types = [NSArray arrayWithObjects:NSStringPboardType, nil];
1293  [app registerServicesMenuSendTypes:types returnTypes:types];
1294}
1295
1296- (Profile*)lastProfile {
1297  // Return the profile of the last-used BrowserWindowController, if available.
1298  if (lastProfile_)
1299    return lastProfile_;
1300
1301  // On first launch, use the logic that ChromeBrowserMain uses to determine
1302  // the initial profile.
1303  ProfileManager* profile_manager = g_browser_process->profile_manager();
1304  if (!profile_manager)
1305    return NULL;
1306
1307  return profile_manager->GetProfile(GetStartupProfilePath(
1308      profile_manager->user_data_dir(),
1309      *CommandLine::ForCurrentProcess()));
1310}
1311
1312- (Profile*)safeLastProfileForNewWindows {
1313  Profile* profile = [self lastProfile];
1314
1315  // Guest sessions must always be OffTheRecord. Use that when opening windows.
1316  if (profile->IsGuestSession())
1317    return profile->GetOffTheRecordProfile();
1318
1319  return profile;
1320}
1321
1322// Various methods to open URLs that we get in a native fashion. We use
1323// StartupBrowserCreator here because on the other platforms, URLs to open come
1324// through the ProcessSingleton, and it calls StartupBrowserCreator. It's best
1325// to bottleneck the openings through that for uniform handling.
1326
1327- (void)openUrls:(const std::vector<GURL>&)urls {
1328  // If the browser hasn't started yet, just queue up the URLs.
1329  if (!startupComplete_) {
1330    startupUrls_.insert(startupUrls_.end(), urls.begin(), urls.end());
1331    return;
1332  }
1333
1334  Browser* browser = chrome::GetLastActiveBrowser();
1335  // if no browser window exists then create one with no tabs to be filled in
1336  if (!browser) {
1337    browser = new Browser(Browser::CreateParams(
1338        [self lastProfile], chrome::HOST_DESKTOP_TYPE_NATIVE));
1339    browser->window()->Show();
1340  }
1341
1342  CommandLine dummy(CommandLine::NO_PROGRAM);
1343  chrome::startup::IsFirstRun first_run = first_run::IsChromeFirstRun() ?
1344      chrome::startup::IS_FIRST_RUN : chrome::startup::IS_NOT_FIRST_RUN;
1345  StartupBrowserCreatorImpl launch(base::FilePath(), dummy, first_run);
1346  launch.OpenURLsInBrowser(browser, false, urls, browser->host_desktop_type());
1347}
1348
1349- (void)getUrl:(NSAppleEventDescriptor*)event
1350     withReply:(NSAppleEventDescriptor*)reply {
1351  NSString* urlStr = [[event paramDescriptorForKeyword:keyDirectObject]
1352                      stringValue];
1353
1354  GURL gurl(base::SysNSStringToUTF8(urlStr));
1355  std::vector<GURL> gurlVector;
1356  gurlVector.push_back(gurl);
1357
1358  [self openUrls:gurlVector];
1359}
1360
1361- (void)application:(NSApplication*)sender
1362          openFiles:(NSArray*)filenames {
1363  std::vector<GURL> gurlVector;
1364  for (NSString* file in filenames) {
1365    GURL gurl =
1366        net::FilePathToFileURL(base::FilePath([file fileSystemRepresentation]));
1367    gurlVector.push_back(gurl);
1368  }
1369  if (!gurlVector.empty())
1370    [self openUrls:gurlVector];
1371  else
1372    NOTREACHED() << "Nothing to open!";
1373
1374  [sender replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
1375}
1376
1377// Show the preferences window, or bring it to the front if it's already
1378// visible.
1379- (IBAction)showPreferences:(id)sender {
1380  if (Browser* browser = ActivateBrowser([self lastProfile])) {
1381    // Show options tab in the active browser window.
1382    chrome::ShowSettings(browser);
1383  } else {
1384    // No browser window, so create one for the options tab.
1385    chrome::OpenOptionsWindow([self safeLastProfileForNewWindows]);
1386  }
1387}
1388
1389- (IBAction)orderFrontStandardAboutPanel:(id)sender {
1390  if (Browser* browser = ActivateBrowser([self lastProfile])) {
1391    chrome::ShowAboutChrome(browser);
1392  } else {
1393    // No browser window, so create one for the about tab.
1394    chrome::OpenAboutWindow([self safeLastProfileForNewWindows]);
1395  }
1396}
1397
1398- (IBAction)toggleConfirmToQuit:(id)sender {
1399  PrefService* prefService = g_browser_process->local_state();
1400  bool enabled = prefService->GetBoolean(prefs::kConfirmToQuitEnabled);
1401  prefService->SetBoolean(prefs::kConfirmToQuitEnabled, !enabled);
1402}
1403
1404- (IBAction)toggleDisplayMessageCenter:(id)sender {
1405  PrefService* prefService = g_browser_process->local_state();
1406  bool enabled = prefService->GetBoolean(prefs::kMessageCenterShowIcon);
1407  prefService->SetBoolean(prefs::kMessageCenterShowIcon, !enabled);
1408}
1409
1410// Explicitly bring to the foreground when creating new windows from the dock.
1411- (void)commandFromDock:(id)sender {
1412  [NSApp activateIgnoringOtherApps:YES];
1413  [self commandDispatch:sender];
1414}
1415
1416- (NSMenu*)applicationDockMenu:(NSApplication*)sender {
1417  NSMenu* dockMenu = [[[NSMenu alloc] initWithTitle: @""] autorelease];
1418  Profile* profile = [self lastProfile];
1419
1420  BOOL profilesAdded = [profileMenuController_ insertItemsIntoMenu:dockMenu
1421                                                          atOffset:0
1422                                                          fromDock:YES];
1423  if (profilesAdded)
1424    [dockMenu addItem:[NSMenuItem separatorItem]];
1425
1426  NSString* titleStr = l10n_util::GetNSStringWithFixup(IDS_NEW_WINDOW_MAC);
1427  base::scoped_nsobject<NSMenuItem> item(
1428      [[NSMenuItem alloc] initWithTitle:titleStr
1429                                 action:@selector(commandFromDock:)
1430                          keyEquivalent:@""]);
1431  [item setTarget:self];
1432  [item setTag:IDC_NEW_WINDOW];
1433  [item setEnabled:[self validateUserInterfaceItem:item]];
1434  [dockMenu addItem:item];
1435
1436  // |profile| can be NULL during unit tests.
1437  if (!profile || !profile->IsSupervised()) {
1438    titleStr = l10n_util::GetNSStringWithFixup(IDS_NEW_INCOGNITO_WINDOW_MAC);
1439    item.reset(
1440        [[NSMenuItem alloc] initWithTitle:titleStr
1441                                   action:@selector(commandFromDock:)
1442                            keyEquivalent:@""]);
1443    [item setTarget:self];
1444    [item setTag:IDC_NEW_INCOGNITO_WINDOW];
1445    [item setEnabled:[self validateUserInterfaceItem:item]];
1446    [dockMenu addItem:item];
1447  }
1448
1449  // TODO(rickcam): Mock out BackgroundApplicationListModel, then add unit
1450  // tests which use the mock in place of the profile-initialized model.
1451
1452  // Avoid breaking unit tests which have no profile.
1453  if (profile) {
1454    BackgroundApplicationListModel applications(profile);
1455    if (applications.size()) {
1456      int position = 0;
1457      NSString* menuStr =
1458          l10n_util::GetNSStringWithFixup(IDS_BACKGROUND_APPS_MAC);
1459      base::scoped_nsobject<NSMenu> appMenu(
1460          [[NSMenu alloc] initWithTitle:menuStr]);
1461      for (extensions::ExtensionList::const_iterator cursor =
1462               applications.begin();
1463           cursor != applications.end();
1464           ++cursor, ++position) {
1465        DCHECK_EQ(applications.GetPosition(cursor->get()), position);
1466        NSString* itemStr =
1467            base::SysUTF16ToNSString(base::UTF8ToUTF16((*cursor)->name()));
1468        base::scoped_nsobject<NSMenuItem> appItem(
1469            [[NSMenuItem alloc] initWithTitle:itemStr
1470                                       action:@selector(executeApplication:)
1471                                keyEquivalent:@""]);
1472        [appItem setTarget:self];
1473        [appItem setTag:position];
1474        [appMenu addItem:appItem];
1475      }
1476    }
1477  }
1478
1479  return dockMenu;
1480}
1481
1482- (const std::vector<GURL>&)startupUrls {
1483  return startupUrls_;
1484}
1485
1486- (BookmarkMenuBridge*)bookmarkMenuBridge {
1487  return bookmarkMenuBridge_.get();
1488}
1489
1490- (void)addObserverForWorkAreaChange:(ui::WorkAreaWatcherObserver*)observer {
1491  workAreaChangeObservers_.AddObserver(observer);
1492}
1493
1494- (void)removeObserverForWorkAreaChange:(ui::WorkAreaWatcherObserver*)observer {
1495  workAreaChangeObservers_.RemoveObserver(observer);
1496}
1497
1498- (void)initAppShimMenuController {
1499  if (!appShimMenuController_)
1500    appShimMenuController_.reset([[AppShimMenuController alloc] init]);
1501}
1502
1503- (void)windowChangedToProfile:(Profile*)profile {
1504  if (lastProfile_ == profile)
1505    return;
1506
1507  // Before tearing down the menu controller bridges, return the Cocoa menus to
1508  // their initial state.
1509  if (bookmarkMenuBridge_.get())
1510    bookmarkMenuBridge_->ResetMenu();
1511  if (historyMenuBridge_.get())
1512    historyMenuBridge_->ResetMenu();
1513
1514  // Rebuild the menus with the new profile.
1515  lastProfile_ = profile;
1516
1517  bookmarkMenuBridge_.reset(new BookmarkMenuBridge(lastProfile_,
1518      [[[NSApp mainMenu] itemWithTag:IDC_BOOKMARKS_MENU] submenu]));
1519  // No need to |BuildMenu| here.  It is done lazily upon menu access.
1520
1521  historyMenuBridge_.reset(new HistoryMenuBridge(lastProfile_));
1522  historyMenuBridge_->BuildMenu();
1523
1524  chrome::BrowserCommandController::
1525      UpdateSharedCommandsForIncognitoAvailability(
1526          menuState_.get(), lastProfile_);
1527  profilePrefRegistrar_.reset(new PrefChangeRegistrar());
1528  profilePrefRegistrar_->Init(lastProfile_->GetPrefs());
1529  profilePrefRegistrar_->Add(
1530      prefs::kIncognitoModeAvailability,
1531      base::Bind(&chrome::BrowserCommandController::
1532                     UpdateSharedCommandsForIncognitoAvailability,
1533                 menuState_.get(),
1534                 lastProfile_));
1535}
1536
1537- (void)applicationDidChangeScreenParameters:(NSNotification*)notification {
1538  // During this callback the working area is not always already updated. Defer.
1539  [self performSelector:@selector(delayedScreenParametersUpdate)
1540             withObject:nil
1541             afterDelay:0];
1542}
1543
1544- (void)delayedScreenParametersUpdate {
1545  FOR_EACH_OBSERVER(ui::WorkAreaWatcherObserver, workAreaChangeObservers_,
1546      WorkAreaChanged());
1547}
1548
1549- (BOOL)application:(NSApplication*)application
1550    willContinueUserActivityWithType:(NSString*)userActivityType {
1551  return [userActivityType isEqualToString:NSUserActivityTypeBrowsingWeb];
1552}
1553
1554- (BOOL)application:(NSApplication*)application
1555    continueUserActivity:(NSUserActivity*)userActivity
1556      restorationHandler:(void (^)(NSArray*))restorationHandler {
1557  if (![userActivity.activityType
1558          isEqualToString:NSUserActivityTypeBrowsingWeb]) {
1559    return NO;
1560  }
1561
1562  NSURL* url = userActivity.webPageURL;
1563  if (!url)
1564    return NO;
1565
1566  GURL gurl(base::SysNSStringToUTF8([url absoluteString]));
1567  std::vector<GURL> gurlVector;
1568  gurlVector.push_back(gurl);
1569
1570  [self openUrls:gurlVector];
1571  return YES;
1572}
1573
1574- (void)application:(NSApplication*)application
1575    didFailToContinueUserActivityWithType:(NSString*)userActivityType
1576                                    error:(NSError*)error {
1577}
1578
1579@end  // @implementation AppController
1580
1581//---------------------------------------------------------------------------
1582
1583namespace app_controller_mac {
1584
1585bool IsOpeningNewWindow() {
1586  return g_is_opening_new_window;
1587}
1588
1589}  // namespace app_controller_mac
1590