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