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