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