app_list_service_mac.mm revision 3551c9c881056c480085172ff9840cab31610854
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <ApplicationServices/ApplicationServices.h>
6#import <Cocoa/Cocoa.h>
7
8#include "apps/app_launcher.h"
9#include "apps/app_shim/app_shim_handler_mac.h"
10#include "apps/app_shim/app_shim_mac.h"
11#include "apps/pref_names.h"
12#include "base/bind.h"
13#include "base/command_line.h"
14#include "base/file_util.h"
15#include "base/lazy_instance.h"
16#include "base/mac/scoped_nsobject.h"
17#include "base/memory/singleton.h"
18#include "base/message_loop/message_loop.h"
19#include "chrome/browser/browser_process.h"
20#include "chrome/browser/extensions/extension_service.h"
21#include "chrome/browser/extensions/extension_system.h"
22#include "chrome/browser/profiles/profile_manager.h"
23#include "chrome/browser/ui/app_list/app_list_controller_delegate.h"
24#include "chrome/browser/ui/app_list/app_list_service.h"
25#include "chrome/browser/ui/app_list/app_list_service_impl.h"
26#include "chrome/browser/ui/app_list/app_list_view_delegate.h"
27#include "chrome/browser/ui/browser_commands.h"
28#include "chrome/browser/ui/extensions/application_launch.h"
29#include "chrome/browser/ui/web_applications/web_app_ui.h"
30#include "chrome/browser/web_applications/web_app.h"
31#include "chrome/browser/web_applications/web_app_mac.h"
32#include "chrome/common/chrome_switches.h"
33#include "chrome/common/chrome_version_info.h"
34#include "chrome/common/mac/app_mode_common.h"
35#include "content/public/browser/browser_thread.h"
36#include "grit/chrome_unscaled_resources.h"
37#include "grit/google_chrome_strings.h"
38#import "ui/app_list/cocoa/app_list_view_controller.h"
39#import "ui/app_list/cocoa/app_list_window_controller.h"
40#include "ui/base/l10n/l10n_util.h"
41#include "ui/base/resource/resource_bundle.h"
42#include "ui/gfx/display.h"
43#include "ui/gfx/screen.h"
44
45namespace gfx {
46class ImageSkia;
47}
48
49namespace {
50
51// Version of the app list shortcut version installed.
52const int kShortcutVersion = 1;
53
54// AppListServiceMac manages global resources needed for the app list to
55// operate, and controls when the app list is opened and closed.
56class AppListServiceMac : public AppListServiceImpl,
57                          public apps::AppShimHandler {
58 public:
59  virtual ~AppListServiceMac() {}
60
61  static AppListServiceMac* GetInstance() {
62    return Singleton<AppListServiceMac,
63                     LeakySingletonTraits<AppListServiceMac> >::get();
64  }
65
66  void ShowWindowNearDock();
67
68  // AppListService overrides:
69  virtual void Init(Profile* initial_profile) OVERRIDE;
70  virtual void CreateForProfile(Profile* requested_profile) OVERRIDE;
71  virtual void ShowForProfile(Profile* requested_profile) OVERRIDE;
72  virtual void DismissAppList() OVERRIDE;
73  virtual bool IsAppListVisible() const OVERRIDE;
74  virtual gfx::NativeWindow GetAppListWindow() OVERRIDE;
75
76  // AppListServiceImpl overrides:
77  virtual void CreateShortcut() OVERRIDE;
78
79  // AppShimHandler overrides:
80  virtual void OnShimLaunch(apps::AppShimHandler::Host* host,
81                            apps::AppShimLaunchType launch_type) OVERRIDE;
82  virtual void OnShimClose(apps::AppShimHandler::Host* host) OVERRIDE;
83  virtual void OnShimFocus(apps::AppShimHandler::Host* host,
84                           apps::AppShimFocusType focus_type) OVERRIDE;
85  virtual void OnShimSetHidden(apps::AppShimHandler::Host* host,
86                               bool hidden) OVERRIDE;
87  virtual void OnShimQuit(apps::AppShimHandler::Host* host) OVERRIDE;
88
89 private:
90  friend struct DefaultSingletonTraits<AppListServiceMac>;
91
92  AppListServiceMac() {}
93
94  base::scoped_nsobject<AppListWindowController> window_controller_;
95  base::scoped_nsobject<NSRunningApplication> previously_active_application_;
96
97  DISALLOW_COPY_AND_ASSIGN(AppListServiceMac);
98};
99
100class AppListControllerDelegateCocoa : public AppListControllerDelegate {
101 public:
102  AppListControllerDelegateCocoa();
103  virtual ~AppListControllerDelegateCocoa();
104
105 private:
106  // AppListControllerDelegate overrides:
107  virtual void DismissView() OVERRIDE;
108  virtual gfx::NativeWindow GetAppListWindow() OVERRIDE;
109  virtual bool CanPin() OVERRIDE;
110  virtual bool CanDoCreateShortcutsFlow(bool is_platform_app) OVERRIDE;
111  virtual void CreateNewWindow(Profile* profile, bool incognito) OVERRIDE;
112  virtual void DoCreateShortcutsFlow(Profile* profile,
113                                     const std::string& extension_id) OVERRIDE;
114  virtual void ActivateApp(Profile* profile,
115                           const extensions::Extension* extension,
116                           int event_flags) OVERRIDE;
117  virtual void LaunchApp(Profile* profile,
118                         const extensions::Extension* extension,
119                         int event_flags) OVERRIDE;
120
121  DISALLOW_COPY_AND_ASSIGN(AppListControllerDelegateCocoa);
122};
123
124ShellIntegration::ShortcutInfo GetAppListShortcutInfo(
125    const base::FilePath& profile_path) {
126  ShellIntegration::ShortcutInfo shortcut_info;
127  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
128  if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
129    shortcut_info.title =
130        l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME_CANARY);
131  } else {
132    shortcut_info.title = l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME);
133  }
134
135  shortcut_info.extension_id = app_mode::kAppListModeId;
136  shortcut_info.description = shortcut_info.title;
137  shortcut_info.profile_path = profile_path;
138
139  return shortcut_info;
140}
141
142void CreateAppListShim(const base::FilePath& profile_path) {
143  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
144  WebApplicationInfo web_app_info;
145  ShellIntegration::ShortcutInfo shortcut_info =
146      GetAppListShortcutInfo(profile_path);
147
148  ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
149  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
150  if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
151#if defined(GOOGLE_CHROME_BUILD)
152    shortcut_info.favicon.Add(
153        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_16));
154    shortcut_info.favicon.Add(
155        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_32));
156    shortcut_info.favicon.Add(
157        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_128));
158    shortcut_info.favicon.Add(
159        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_256));
160#else
161    NOTREACHED();
162#endif
163  } else {
164    shortcut_info.favicon.Add(
165        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16));
166    shortcut_info.favicon.Add(
167        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32));
168    shortcut_info.favicon.Add(
169        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128));
170    shortcut_info.favicon.Add(
171        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256));
172  }
173
174  ShellIntegration::ShortcutLocations shortcut_locations;
175  PrefService* local_state = g_browser_process->local_state();
176  int installed_version =
177      local_state->GetInteger(apps::prefs::kAppLauncherShortcutVersion);
178
179  // If this is a first-time install, add a dock icon. Otherwise just update
180  // the target, and wait for OSX to refresh its icon caches. This might not
181  // occur until a reboot, but OSX does not offer a nicer way. Deleting cache
182  // files on disk and killing processes can easily result in icon corruption.
183  if (installed_version == 0)
184    shortcut_locations.in_quick_launch_bar = true;
185
186  web_app::CreateShortcuts(shortcut_info,
187                           shortcut_locations,
188                           web_app::SHORTCUT_CREATION_AUTOMATED);
189
190  local_state->SetInteger(apps::prefs::kAppLauncherShortcutVersion,
191                          kShortcutVersion);
192}
193
194// Check that there is an app list shim. If enabling and there is not, make one.
195// If the flag is not present, and there is a shim, delete it.
196void CheckAppListShimOnFileThread(const base::FilePath& profile_path) {
197  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
198  const bool enable =
199      CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppListShim);
200  base::FilePath install_path = web_app::GetAppInstallPath(
201      GetAppListShortcutInfo(profile_path));
202  if (enable == base::PathExists(install_path))
203    return;
204
205  if (enable) {
206    content::BrowserThread::PostTask(
207        content::BrowserThread::UI, FROM_HERE,
208        base::Bind(&CreateAppListShim, profile_path));
209    return;
210  }
211
212  // Sanity check because deleting things recursively is scary.
213  CHECK(install_path.MatchesExtension(".app"));
214  base::DeleteFile(install_path, true /* recursive */);
215}
216
217void CreateShortcutsInDefaultLocation(
218    const ShellIntegration::ShortcutInfo& shortcut_info) {
219  web_app::CreateShortcuts(shortcut_info,
220                           ShellIntegration::ShortcutLocations(),
221                           web_app::SHORTCUT_CREATION_BY_USER);
222}
223
224NSRunningApplication* ActiveApplicationNotChrome() {
225  NSArray* applications = [[NSWorkspace sharedWorkspace] runningApplications];
226  for (NSRunningApplication* application in applications) {
227    if (![application isActive])
228      continue;
229
230    if ([application isEqual:[NSRunningApplication currentApplication]])
231      return nil;  // Chrome is active.
232
233    return application;
234  }
235
236  return nil;
237}
238
239AppListControllerDelegateCocoa::AppListControllerDelegateCocoa() {}
240
241AppListControllerDelegateCocoa::~AppListControllerDelegateCocoa() {}
242
243void AppListControllerDelegateCocoa::DismissView() {
244  AppListServiceMac::GetInstance()->DismissAppList();
245}
246
247gfx::NativeWindow AppListControllerDelegateCocoa::GetAppListWindow() {
248  return AppListServiceMac::GetInstance()->GetAppListWindow();
249}
250
251bool AppListControllerDelegateCocoa::CanPin() {
252  return false;
253}
254
255bool AppListControllerDelegateCocoa::CanDoCreateShortcutsFlow(
256    bool is_platform_app) {
257  return false;
258}
259
260void AppListControllerDelegateCocoa::DoCreateShortcutsFlow(
261    Profile* profile, const std::string& extension_id) {
262  ExtensionService* service =
263      extensions::ExtensionSystem::Get(profile)->extension_service();
264  DCHECK(service);
265  const extensions::Extension* extension =
266      service->GetInstalledExtension(extension_id);
267  DCHECK(extension);
268
269  web_app::UpdateShortcutInfoAndIconForApp(
270      *extension, profile, base::Bind(&CreateShortcutsInDefaultLocation));
271}
272
273void AppListControllerDelegateCocoa::CreateNewWindow(
274    Profile* profile, bool incognito) {
275  Profile* window_profile = incognito ?
276      profile->GetOffTheRecordProfile() : profile;
277  chrome::NewEmptyWindow(window_profile, chrome::GetActiveDesktop());
278}
279
280void AppListControllerDelegateCocoa::ActivateApp(
281    Profile* profile, const extensions::Extension* extension, int event_flags) {
282  LaunchApp(profile, extension, event_flags);
283}
284
285void AppListControllerDelegateCocoa::LaunchApp(
286    Profile* profile, const extensions::Extension* extension, int event_flags) {
287  chrome::OpenApplication(chrome::AppLaunchParams(
288      profile, extension, NEW_FOREGROUND_TAB));
289}
290
291void AppListServiceMac::Init(Profile* initial_profile) {
292  // On Mac, Init() is called multiple times for a process: any time there is no
293  // browser window open and a new window is opened, and during process startup
294  // to handle the silent launch case (e.g. for app shims). In the startup case,
295  // a profile has not yet been determined so |initial_profile| will be NULL.
296  static bool init_called_with_profile = false;
297  if (initial_profile && !init_called_with_profile) {
298    init_called_with_profile = true;
299    HandleCommandLineFlags(initial_profile);
300    PrefService* local_state = g_browser_process->local_state();
301    if (!apps::IsAppLauncherEnabled()) {
302      local_state->SetInteger(apps::prefs::kAppLauncherShortcutVersion, 0);
303
304      // Not yet enabled via the Web Store. Check for the chrome://flag.
305      content::BrowserThread::PostTask(
306          content::BrowserThread::FILE, FROM_HERE,
307          base::Bind(&CheckAppListShimOnFileThread,
308                     initial_profile->GetPath()));
309    } else {
310      int installed_shortcut_version =
311          local_state->GetInteger(apps::prefs::kAppLauncherShortcutVersion);
312
313      if (kShortcutVersion > installed_shortcut_version)
314        CreateShortcut();
315    }
316  }
317
318  static bool init_called = false;
319  if (init_called)
320    return;
321
322  init_called = true;
323  apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId,
324                                        AppListServiceMac::GetInstance());
325}
326
327void AppListServiceMac::CreateForProfile(Profile* requested_profile) {
328  if (profile() == requested_profile)
329    return;
330
331  // The Objective C objects might be released at some unknown point in the
332  // future, so explicitly clear references to C++ objects.
333  [[window_controller_ appListViewController]
334      setDelegate:scoped_ptr<app_list::AppListViewDelegate>()];
335
336  SetProfile(requested_profile);
337  scoped_ptr<app_list::AppListViewDelegate> delegate(
338      new AppListViewDelegate(new AppListControllerDelegateCocoa(), profile()));
339  window_controller_.reset([[AppListWindowController alloc] init]);
340  [[window_controller_ appListViewController] setDelegate:delegate.Pass()];
341}
342
343void AppListServiceMac::ShowForProfile(Profile* requested_profile) {
344  if (requested_profile->IsManaged())
345    return;
346
347  InvalidatePendingProfileLoads();
348
349  if (IsAppListVisible() && (requested_profile == profile())) {
350    ShowWindowNearDock();
351    return;
352  }
353
354  SetProfilePath(requested_profile->GetPath());
355
356  DismissAppList();
357  CreateForProfile(requested_profile);
358  ShowWindowNearDock();
359}
360
361void AppListServiceMac::DismissAppList() {
362  if (!IsAppListVisible())
363    return;
364
365  // If the app list is currently the main window, it will activate the next
366  // Chrome window when dismissed. But if a different application was active
367  // when the app list was shown, activate that instead.
368  base::scoped_nsobject<NSRunningApplication> prior_app;
369  if ([[window_controller_ window] isMainWindow])
370    prior_app.swap(previously_active_application_);
371  else
372    previously_active_application_.reset();
373
374  // If activation is successful, the app list will lose main status and try to
375  // close itself again. It can't be closed in this runloop iteration without
376  // OSX deciding to raise the next Chrome window, and _then_ activating the
377  // application on top. This also occurs if no activation option is given.
378  if ([prior_app activateWithOptions:NSApplicationActivateIgnoringOtherApps])
379    return;
380
381  [[window_controller_ window] close];
382}
383
384bool AppListServiceMac::IsAppListVisible() const {
385  return [[window_controller_ window] isVisible];
386}
387
388void AppListServiceMac::CreateShortcut() {
389  CreateAppListShim(GetProfilePath(
390      g_browser_process->profile_manager()->user_data_dir()));
391}
392
393NSWindow* AppListServiceMac::GetAppListWindow() {
394  return [window_controller_ window];
395}
396
397void AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host,
398                                     apps::AppShimLaunchType launch_type) {
399  if (IsAppListVisible())
400    DismissAppList();
401  else
402    Show();
403
404  // Always close the shim process immediately.
405  host->OnAppLaunchComplete(apps::APP_SHIM_LAUNCH_DUPLICATE_HOST);
406}
407
408void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {}
409
410void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host,
411                                    apps::AppShimFocusType focus_type) {}
412
413void AppListServiceMac::OnShimSetHidden(apps::AppShimHandler::Host* host,
414                                        bool hidden) {}
415
416void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {}
417
418enum DockLocation {
419  DockLocationOtherDisplay,
420  DockLocationBottom,
421  DockLocationLeft,
422  DockLocationRight,
423};
424
425DockLocation DockLocationInDisplay(const gfx::Display& display) {
426  // Assume the dock occupies part of the work area either on the left, right or
427  // bottom of the display. Note in the autohide case, it is always 4 pixels.
428  const gfx::Rect work_area = display.work_area();
429  const gfx::Rect display_bounds = display.bounds();
430  if (work_area.bottom() != display_bounds.bottom())
431    return DockLocationBottom;
432
433  if (work_area.x() != display_bounds.x())
434    return DockLocationLeft;
435
436  if (work_area.right() != display_bounds.right())
437    return DockLocationRight;
438
439  return DockLocationOtherDisplay;
440}
441
442// If |work_area_edge| is too close to the |screen_edge| (e.g. autohide dock),
443// adjust |anchor| away from the edge by a constant amount to reduce overlap and
444// ensure the dock icon can still be clicked to dismiss the app list.
445int AdjustPointForDynamicDock(int anchor, int screen_edge, int work_area_edge) {
446  const int kAutohideDockThreshold = 10;
447  const int kExtraDistance = 50;  // A dock with 40 items is about this size.
448  if (abs(work_area_edge - screen_edge) > kAutohideDockThreshold)
449    return anchor;
450
451  return anchor +
452      (screen_edge < work_area_edge ? kExtraDistance : -kExtraDistance);
453}
454
455NSPoint GetAppListWindowOrigin(NSWindow* window) {
456  gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]);
457  // Ensure y coordinates are flipped back into AppKit's coordinate system.
458  const CGFloat max_y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]);
459  if (!CGCursorIsVisible()) {
460    // If Chrome is the active application, display on the same display as
461    // Chrome's keyWindow since this will catch activations triggered, e.g, via
462    // WebStore install. If another application is active, OSX doesn't provide a
463    // reliable way to get the display in use. Fall back to the primary display
464    // since it has the menu bar and is likely to be correct, e.g., for
465    // activations from Spotlight.
466    const gfx::NativeView key_view = [[NSApp keyWindow] contentView];
467    const gfx::Rect work_area = key_view && [NSApp isActive] ?
468        screen->GetDisplayNearestWindow(key_view).work_area() :
469        screen->GetPrimaryDisplay().work_area();
470    return NSMakePoint(work_area.x(), max_y - work_area.bottom());
471  }
472
473  gfx::Point anchor = screen->GetCursorScreenPoint();
474  const gfx::Display display = screen->GetDisplayNearestPoint(anchor);
475  const DockLocation dock_location = DockLocationInDisplay(display);
476  const gfx::Rect display_bounds = display.bounds();
477
478  if (dock_location == DockLocationOtherDisplay) {
479    // Just display at the bottom-left of the display the cursor is on.
480    return NSMakePoint(display_bounds.x(), max_y - display_bounds.bottom());
481  }
482
483  // Anchor the center of the window in a region that prevents the window
484  // showing outside of the work area.
485  const NSSize window_size = [window frame].size;
486  const gfx::Rect work_area = display.work_area();
487  gfx::Rect anchor_area = work_area;
488  anchor_area.Inset(window_size.width / 2, window_size.height / 2);
489  anchor.SetToMax(anchor_area.origin());
490  anchor.SetToMin(anchor_area.bottom_right());
491
492  // Move anchor to the dock, keeping the other axis aligned with the cursor.
493  switch (dock_location) {
494    case DockLocationBottom:
495      anchor.set_y(AdjustPointForDynamicDock(
496          anchor_area.bottom(), display_bounds.bottom(), work_area.bottom()));
497      break;
498    case DockLocationLeft:
499      anchor.set_x(AdjustPointForDynamicDock(
500          anchor_area.x(), display_bounds.x(), work_area.x()));
501      break;
502    case DockLocationRight:
503      anchor.set_x(AdjustPointForDynamicDock(
504          anchor_area.right(), display_bounds.right(), work_area.right()));
505      break;
506    default:
507      NOTREACHED();
508  }
509
510  return NSMakePoint(
511      anchor.x() - window_size.width / 2,
512      max_y - anchor.y() - window_size.height / 2);
513}
514
515void AppListServiceMac::ShowWindowNearDock() {
516  NSWindow* window = GetAppListWindow();
517  DCHECK(window);
518  [window setFrameOrigin:GetAppListWindowOrigin(window)];
519
520  // Before activating, see if an application other than Chrome is currently the
521  // active application, so that it can be reactivated when dismissing.
522  previously_active_application_.reset([ActiveApplicationNotChrome() retain]);
523
524  [window makeKeyAndOrderFront:nil];
525  [NSApp activateIgnoringOtherApps:YES];
526}
527
528}  // namespace
529
530// static
531AppListService* AppListService::Get() {
532  return AppListServiceMac::GetInstance();
533}
534
535// static
536void AppListService::InitAll(Profile* initial_profile) {
537  Get()->Init(initial_profile);
538}
539