app_list_service_mac.mm revision 9ab5563a3196760eb381d102cbb2bc0f7abc6a50
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_shim/app_shim_handler_mac.h"
9#include "base/bind.h"
10#include "base/command_line.h"
11#include "base/file_util.h"
12#include "base/lazy_instance.h"
13#include "base/mac/scoped_nsobject.h"
14#include "base/memory/singleton.h"
15#include "base/message_loop/message_loop.h"
16#include "base/observer_list.h"
17#include "chrome/browser/extensions/extension_service.h"
18#include "chrome/browser/extensions/extension_system.h"
19#include "chrome/browser/ui/app_list/app_list_controller_delegate.h"
20#include "chrome/browser/ui/app_list/app_list_service.h"
21#include "chrome/browser/ui/app_list/app_list_service_impl.h"
22#include "chrome/browser/ui/app_list/app_list_view_delegate.h"
23#include "chrome/browser/ui/extensions/application_launch.h"
24#include "chrome/browser/ui/web_applications/web_app_ui.h"
25#include "chrome/browser/web_applications/web_app.h"
26#include "chrome/browser/web_applications/web_app_mac.h"
27#include "chrome/common/chrome_switches.h"
28#include "chrome/common/chrome_version_info.h"
29#include "chrome/common/mac/app_mode_common.h"
30#include "content/public/browser/browser_thread.h"
31#include "grit/chrome_unscaled_resources.h"
32#include "grit/google_chrome_strings.h"
33#import "ui/app_list/cocoa/app_list_view_controller.h"
34#import "ui/app_list/cocoa/app_list_window_controller.h"
35#include "ui/base/l10n/l10n_util.h"
36#include "ui/base/resource/resource_bundle.h"
37#include "ui/gfx/display.h"
38#include "ui/gfx/screen.h"
39
40namespace gfx {
41class ImageSkia;
42}
43
44namespace {
45
46// AppListServiceMac manages global resources needed for the app list to
47// operate, and controls when the app list is opened and closed.
48class AppListServiceMac : public AppListServiceImpl,
49                          public apps::AppShimHandler {
50 public:
51  virtual ~AppListServiceMac() {}
52
53  static AppListServiceMac* GetInstance() {
54    return Singleton<AppListServiceMac,
55                     LeakySingletonTraits<AppListServiceMac> >::get();
56  }
57
58  void CreateAppList(Profile* profile);
59  void ShowWindowNearDock();
60
61  // AppListService overrides:
62  virtual void Init(Profile* initial_profile) OVERRIDE;
63  virtual void ShowAppList(Profile* requested_profile) OVERRIDE;
64  virtual void DismissAppList() OVERRIDE;
65  virtual bool IsAppListVisible() const OVERRIDE;
66  virtual void EnableAppList(Profile* initial_profile) OVERRIDE;
67  virtual gfx::NativeWindow GetAppListWindow() OVERRIDE;
68
69  // AppListServiceImpl override:
70  virtual void OnSigninStatusChanged() OVERRIDE;
71
72  // AppShimHandler overrides:
73  virtual void OnShimLaunch(apps::AppShimHandler::Host* host,
74                            apps::AppShimLaunchType launch_type) OVERRIDE;
75  virtual void OnShimClose(apps::AppShimHandler::Host* host) OVERRIDE;
76  virtual void OnShimFocus(apps::AppShimHandler::Host* host,
77                           apps::AppShimFocusType focus_type) OVERRIDE;
78  virtual void OnShimSetHidden(apps::AppShimHandler::Host* host,
79                               bool hidden) OVERRIDE;
80  virtual void OnShimQuit(apps::AppShimHandler::Host* host) OVERRIDE;
81
82 private:
83  friend struct DefaultSingletonTraits<AppListServiceMac>;
84
85  AppListServiceMac() {}
86
87  base::scoped_nsobject<AppListWindowController> window_controller_;
88
89  // App shim hosts observing when the app list is dismissed. In normal user
90  // usage there should only be one. However, it can't be guaranteed, so use
91  // an ObserverList rather than handling corner cases.
92  ObserverList<apps::AppShimHandler::Host> observers_;
93
94  DISALLOW_COPY_AND_ASSIGN(AppListServiceMac);
95};
96
97class AppListControllerDelegateCocoa : public AppListControllerDelegate {
98 public:
99  AppListControllerDelegateCocoa();
100  virtual ~AppListControllerDelegateCocoa();
101
102 private:
103  // AppListControllerDelegate overrides:
104  virtual void DismissView() OVERRIDE;
105  virtual gfx::NativeWindow GetAppListWindow() OVERRIDE;
106  virtual bool CanPin() OVERRIDE;
107  virtual bool CanDoCreateShortcutsFlow(bool is_platform_app) OVERRIDE;
108  virtual void DoCreateShortcutsFlow(Profile* profile,
109                                     const std::string& extension_id) OVERRIDE;
110  virtual void ActivateApp(Profile* profile,
111                           const extensions::Extension* extension,
112                           int event_flags) OVERRIDE;
113  virtual void LaunchApp(Profile* profile,
114                         const extensions::Extension* extension,
115                         int event_flags) OVERRIDE;
116
117  DISALLOW_COPY_AND_ASSIGN(AppListControllerDelegateCocoa);
118};
119
120ShellIntegration::ShortcutInfo GetAppListShortcutInfo(
121    const base::FilePath& profile_path) {
122  ShellIntegration::ShortcutInfo shortcut_info;
123  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
124  if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
125    shortcut_info.title =
126        l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME_CANARY);
127  } else {
128    shortcut_info.title = l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME);
129  }
130
131  shortcut_info.extension_id = app_mode::kAppListModeId;
132  shortcut_info.description = shortcut_info.title;
133  shortcut_info.profile_path = profile_path;
134
135  return shortcut_info;
136}
137
138void CreateAppListShim(const base::FilePath& profile_path) {
139  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
140  WebApplicationInfo web_app_info;
141  ShellIntegration::ShortcutInfo shortcut_info =
142      GetAppListShortcutInfo(profile_path);
143
144  ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
145  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
146  if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
147#if defined(GOOGLE_CHROME_BUILD)
148    shortcut_info.favicon.Add(
149        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_16));
150    shortcut_info.favicon.Add(
151        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_32));
152    shortcut_info.favicon.Add(
153        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_128));
154    shortcut_info.favicon.Add(
155        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_256));
156#else
157    NOTREACHED();
158#endif
159  } else {
160    shortcut_info.favicon.Add(
161        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16));
162    shortcut_info.favicon.Add(
163        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32));
164    shortcut_info.favicon.Add(
165        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128));
166    shortcut_info.favicon.Add(
167        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256));
168  }
169
170  // TODO(tapted): Create a dock icon using chrome/browser/mac/dock.h .
171  web_app::CreateShortcuts(shortcut_info,
172                           ShellIntegration::ShortcutLocations(),
173                           web_app::ALLOW_DUPLICATE_SHORTCUTS);
174}
175
176// Check that there is an app list shim. If enabling and there is not, make one.
177// If disabling with --enable-app-list-shim=0, and there is one, delete it.
178void CheckAppListShimOnFileThread(const base::FilePath& profile_path) {
179  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
180  const bool enable =
181      CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppListShim);
182  base::FilePath install_path = web_app::GetAppInstallPath(
183      GetAppListShortcutInfo(profile_path));
184  if (enable == base::PathExists(install_path))
185    return;
186
187  if (enable) {
188    content::BrowserThread::PostTask(
189        content::BrowserThread::UI, FROM_HERE,
190        base::Bind(&CreateAppListShim, profile_path));
191    return;
192  }
193
194  // Sanity check because deleting things recursively is scary.
195  CHECK(install_path.MatchesExtension(".app"));
196  base::DeleteFile(install_path, true /* recursive */);
197}
198
199void CreateShortcutsInDefaultLocation(
200    const ShellIntegration::ShortcutInfo& shortcut_info) {
201  web_app::CreateShortcuts(shortcut_info,
202                           ShellIntegration::ShortcutLocations(),
203                           web_app::ALLOW_DUPLICATE_SHORTCUTS);
204}
205
206AppListControllerDelegateCocoa::AppListControllerDelegateCocoa() {}
207
208AppListControllerDelegateCocoa::~AppListControllerDelegateCocoa() {}
209
210void AppListControllerDelegateCocoa::DismissView() {
211  AppListServiceMac::GetInstance()->DismissAppList();
212}
213
214gfx::NativeWindow AppListControllerDelegateCocoa::GetAppListWindow() {
215  return AppListServiceMac::GetInstance()->GetAppListWindow();
216}
217
218bool AppListControllerDelegateCocoa::CanPin() {
219  return false;
220}
221
222bool AppListControllerDelegateCocoa::CanDoCreateShortcutsFlow(
223    bool is_platform_app) {
224  return is_platform_app &&
225      CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppShims);
226}
227
228void AppListControllerDelegateCocoa::DoCreateShortcutsFlow(
229    Profile* profile, const std::string& extension_id) {
230  ExtensionService* service =
231      extensions::ExtensionSystem::Get(profile)->extension_service();
232  DCHECK(service);
233  const extensions::Extension* extension =
234      service->GetInstalledExtension(extension_id);
235  DCHECK(extension);
236
237  web_app::UpdateShortcutInfoAndIconForApp(
238      *extension, profile, base::Bind(&CreateShortcutsInDefaultLocation));
239}
240
241void AppListControllerDelegateCocoa::ActivateApp(
242    Profile* profile, const extensions::Extension* extension, int event_flags) {
243  LaunchApp(profile, extension, event_flags);
244}
245
246void AppListControllerDelegateCocoa::LaunchApp(
247    Profile* profile, const extensions::Extension* extension, int event_flags) {
248  chrome::OpenApplication(chrome::AppLaunchParams(
249      profile, extension, NEW_FOREGROUND_TAB));
250}
251
252void AppListServiceMac::CreateAppList(Profile* requested_profile) {
253  if (profile() == requested_profile)
254    return;
255
256  // The Objective C objects might be released at some unknown point in the
257  // future, so explicitly clear references to C++ objects.
258  [[window_controller_ appListViewController]
259      setDelegate:scoped_ptr<app_list::AppListViewDelegate>()];
260
261  SetProfile(requested_profile);
262  scoped_ptr<app_list::AppListViewDelegate> delegate(
263      new AppListViewDelegate(new AppListControllerDelegateCocoa(), profile()));
264  window_controller_.reset([[AppListWindowController alloc] init]);
265  [[window_controller_ appListViewController] setDelegate:delegate.Pass()];
266}
267
268void AppListServiceMac::Init(Profile* initial_profile) {
269  // On Mac, Init() is called multiple times for a process: any time there is no
270  // browser window open and a new window is opened, and during process startup
271  // to handle the silent launch case (e.g. for app shims). In the startup case,
272  // a profile has not yet been determined so |initial_profile| will be NULL.
273  if (initial_profile) {
274    static bool checked_shim = false;
275    if (!checked_shim) {
276      checked_shim = true;
277      content::BrowserThread::PostTask(
278          content::BrowserThread::FILE, FROM_HERE,
279          base::Bind(&CheckAppListShimOnFileThread,
280                     initial_profile->GetPath()));
281    }
282  }
283
284  static bool init_called = false;
285  if (init_called)
286    return;
287
288  init_called = true;
289  apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId,
290                                        AppListServiceMac::GetInstance());
291}
292
293void AppListServiceMac::ShowAppList(Profile* requested_profile) {
294  InvalidatePendingProfileLoads();
295
296  if (IsAppListVisible() && (requested_profile == profile())) {
297    ShowWindowNearDock();
298    return;
299  }
300
301  SaveProfilePathToLocalState(requested_profile->GetPath());
302
303  DismissAppList();
304  CreateAppList(requested_profile);
305  ShowWindowNearDock();
306}
307
308void AppListServiceMac::DismissAppList() {
309  if (!IsAppListVisible())
310    return;
311
312  [[window_controller_ window] close];
313
314  FOR_EACH_OBSERVER(apps::AppShimHandler::Host,
315                    observers_,
316                    OnAppClosed());
317}
318
319bool AppListServiceMac::IsAppListVisible() const {
320  return [[window_controller_ window] isVisible];
321}
322
323void AppListServiceMac::EnableAppList(Profile* initial_profile) {
324  // TODO(tapted): Implement enable logic here for OSX.
325}
326
327NSWindow* AppListServiceMac::GetAppListWindow() {
328  return [window_controller_ window];
329}
330
331void AppListServiceMac::OnSigninStatusChanged() {
332  [[window_controller_ appListViewController] onSigninStatusChanged];
333}
334
335void AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host,
336                                     apps::AppShimLaunchType launch_type) {
337  ShowForSavedProfile();
338  observers_.AddObserver(host);
339  host->OnAppLaunchComplete(apps::APP_SHIM_LAUNCH_SUCCESS);
340}
341
342void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {
343  observers_.RemoveObserver(host);
344  DismissAppList();
345}
346
347void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host,
348                                    apps::AppShimFocusType focus_type) {
349  DismissAppList();
350}
351
352void AppListServiceMac::OnShimSetHidden(apps::AppShimHandler::Host* host,
353                                        bool hidden) {}
354
355void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {
356  DismissAppList();
357}
358
359enum DockLocation {
360  DockLocationOtherDisplay,
361  DockLocationBottom,
362  DockLocationLeft,
363  DockLocationRight,
364};
365
366DockLocation DockLocationInDisplay(const gfx::Display& display) {
367  // Assume the dock occupies part of the work area either on the left, right or
368  // bottom of the display. Note in the autohide case, it is always 4 pixels.
369  const gfx::Rect work_area = display.work_area();
370  const gfx::Rect display_bounds = display.bounds();
371  if (work_area.bottom() != display_bounds.bottom())
372    return DockLocationBottom;
373
374  if (work_area.x() != display_bounds.x())
375    return DockLocationLeft;
376
377  if (work_area.right() != display_bounds.right())
378    return DockLocationRight;
379
380  return DockLocationOtherDisplay;
381}
382
383// If |work_area_edge| is too close to the |screen_edge| (e.g. autohide dock),
384// adjust |anchor| away from the edge by a constant amount to reduce overlap and
385// ensure the dock icon can still be clicked to dismiss the app list.
386int AdjustPointForDynamicDock(int anchor, int screen_edge, int work_area_edge) {
387  const int kAutohideDockThreshold = 10;
388  const int kExtraDistance = 50;  // A dock with 40 items is about this size.
389  if (abs(work_area_edge - screen_edge) > kAutohideDockThreshold)
390    return anchor;
391
392  return anchor +
393      (screen_edge < work_area_edge ? kExtraDistance : -kExtraDistance);
394}
395
396NSPoint GetAppListWindowOrigin(NSWindow* window) {
397  gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]);
398  // Ensure y coordinates are flipped back into AppKit's coordinate system.
399  const CGFloat max_y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]);
400  if (!CGCursorIsVisible()) {
401    // If Chrome is the active application, display on the same display as
402    // Chrome's keyWindow since this will catch activations triggered, e.g, via
403    // WebStore install. If another application is active, OSX doesn't provide a
404    // reliable way to get the display in use. Fall back to the primary display
405    // since it has the menu bar and is likely to be correct, e.g., for
406    // activations from Spotlight.
407    const gfx::NativeView key_view = [[NSApp keyWindow] contentView];
408    const gfx::Rect work_area = key_view && [NSApp isActive] ?
409        screen->GetDisplayNearestWindow(key_view).work_area() :
410        screen->GetPrimaryDisplay().work_area();
411    return NSMakePoint(work_area.x(), max_y - work_area.bottom());
412  }
413
414  gfx::Point anchor = screen->GetCursorScreenPoint();
415  const gfx::Display display = screen->GetDisplayNearestPoint(anchor);
416  const DockLocation dock_location = DockLocationInDisplay(display);
417  const gfx::Rect display_bounds = display.bounds();
418
419  if (dock_location == DockLocationOtherDisplay) {
420    // Just display at the bottom-left of the display the cursor is on.
421    return NSMakePoint(display_bounds.x(), max_y - display_bounds.bottom());
422  }
423
424  // Anchor the center of the window in a region that prevents the window
425  // showing outside of the work area.
426  const NSSize window_size = [window frame].size;
427  const gfx::Rect work_area = display.work_area();
428  gfx::Rect anchor_area = work_area;
429  anchor_area.Inset(window_size.width / 2, window_size.height / 2);
430  anchor.SetToMax(anchor_area.origin());
431  anchor.SetToMin(anchor_area.bottom_right());
432
433  // Move anchor to the dock, keeping the other axis aligned with the cursor.
434  switch (dock_location) {
435    case DockLocationBottom:
436      anchor.set_y(AdjustPointForDynamicDock(
437          anchor_area.bottom(), display_bounds.bottom(), work_area.bottom()));
438      break;
439    case DockLocationLeft:
440      anchor.set_x(AdjustPointForDynamicDock(
441          anchor_area.x(), display_bounds.x(), work_area.x()));
442      break;
443    case DockLocationRight:
444      anchor.set_x(AdjustPointForDynamicDock(
445          anchor_area.right(), display_bounds.right(), work_area.right()));
446      break;
447    default:
448      NOTREACHED();
449  }
450
451  return NSMakePoint(
452      anchor.x() - window_size.width / 2,
453      max_y - anchor.y() - window_size.height / 2);
454}
455
456void AppListServiceMac::ShowWindowNearDock() {
457  NSWindow* window = GetAppListWindow();
458  DCHECK(window);
459  [window setFrameOrigin:GetAppListWindowOrigin(window)];
460  [window makeKeyAndOrderFront:nil];
461  [NSApp activateIgnoringOtherApps:YES];
462}
463
464}  // namespace
465
466// static
467AppListService* AppListService::Get() {
468  return AppListServiceMac::GetInstance();
469}
470
471// static
472void AppListService::InitAll(Profile* initial_profile) {
473  Get()->Init(initial_profile);
474}
475