app_list_service_mac.mm revision 868fa2fe829687343ffae624259930155e16dbd8
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 "apps/app_shim/app_shim_handler_mac.h"
6#include "base/bind.h"
7#include "base/command_line.h"
8#include "base/file_util.h"
9#include "base/lazy_instance.h"
10#include "base/memory/scoped_nsobject.h"
11#include "base/memory/singleton.h"
12#include "base/message_loop.h"
13#include "base/observer_list.h"
14#include "chrome/browser/ui/app_list/app_list_controller_delegate.h"
15#include "chrome/browser/ui/app_list/app_list_service.h"
16#include "chrome/browser/ui/app_list/app_list_service_impl.h"
17#include "chrome/browser/ui/app_list/app_list_view_delegate.h"
18#include "chrome/browser/ui/extensions/application_launch.h"
19#include "chrome/browser/web_applications/web_app.h"
20#include "chrome/browser/web_applications/web_app_mac.h"
21#include "chrome/common/chrome_switches.h"
22#include "chrome/common/chrome_version_info.h"
23#include "chrome/common/mac/app_mode_common.h"
24#include "content/public/browser/browser_thread.h"
25#include "grit/chrome_unscaled_resources.h"
26#include "grit/google_chrome_strings.h"
27#import "ui/app_list/cocoa/app_list_view_controller.h"
28#import "ui/app_list/cocoa/app_list_window_controller.h"
29#include "ui/base/l10n/l10n_util.h"
30#include "ui/base/resource/resource_bundle.h"
31#include "ui/gfx/display.h"
32#include "ui/gfx/screen.h"
33
34namespace gfx {
35class ImageSkia;
36}
37
38namespace {
39
40// AppListServiceMac manages global resources needed for the app list to
41// operate, and controls when the app list is opened and closed.
42class AppListServiceMac : public AppListServiceImpl,
43                          public apps::AppShimHandler {
44 public:
45  virtual ~AppListServiceMac() {}
46
47  static AppListServiceMac* GetInstance() {
48    return Singleton<AppListServiceMac,
49                     LeakySingletonTraits<AppListServiceMac> >::get();
50  }
51
52  void CreateAppList(Profile* profile);
53  void ShowWindowNearDock();
54
55  // AppListService overrides:
56  virtual void Init(Profile* initial_profile) OVERRIDE;
57  virtual void ShowAppList(Profile* requested_profile) OVERRIDE;
58  virtual void DismissAppList() OVERRIDE;
59  virtual bool IsAppListVisible() const OVERRIDE;
60  virtual void EnableAppList() OVERRIDE;
61  virtual gfx::NativeWindow GetAppListWindow() OVERRIDE;
62
63  // AppShimHandler overrides:
64  virtual bool OnShimLaunch(apps::AppShimHandler::Host* host,
65                            apps::AppShimLaunchType launch_type) OVERRIDE;
66  virtual void OnShimClose(apps::AppShimHandler::Host* host) OVERRIDE;
67  virtual void OnShimFocus(apps::AppShimHandler::Host* host) OVERRIDE;
68  virtual void OnShimQuit(apps::AppShimHandler::Host* host) OVERRIDE;
69
70 private:
71  friend struct DefaultSingletonTraits<AppListServiceMac>;
72
73  AppListServiceMac() {}
74
75  scoped_nsobject<AppListWindowController> window_controller_;
76
77  // App shim hosts observing when the app list is dismissed. In normal user
78  // usage there should only be one. However, it can't be guaranteed, so use
79  // an ObserverList rather than handling corner cases.
80  ObserverList<apps::AppShimHandler::Host> observers_;
81
82  DISALLOW_COPY_AND_ASSIGN(AppListServiceMac);
83};
84
85class AppListControllerDelegateCocoa : public AppListControllerDelegate {
86 public:
87  AppListControllerDelegateCocoa();
88  virtual ~AppListControllerDelegateCocoa();
89
90 private:
91  // AppListControllerDelegate overrides:
92  virtual void DismissView() OVERRIDE;
93  virtual gfx::NativeWindow GetAppListWindow() OVERRIDE;
94  virtual bool CanPin() OVERRIDE;
95  virtual bool CanShowCreateShortcutsDialog() OVERRIDE;
96  virtual void ActivateApp(Profile* profile,
97                           const extensions::Extension* extension,
98                           int event_flags) OVERRIDE;
99  virtual void LaunchApp(Profile* profile,
100                         const extensions::Extension* extension,
101                         int event_flags) OVERRIDE;
102
103  DISALLOW_COPY_AND_ASSIGN(AppListControllerDelegateCocoa);
104};
105
106ShellIntegration::ShortcutInfo GetAppListShortcutInfo(
107    const base::FilePath& profile_path) {
108  ShellIntegration::ShortcutInfo shortcut_info;
109  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
110  if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
111    shortcut_info.title =
112        l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME_CANARY);
113  } else {
114    shortcut_info.title = l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME);
115  }
116
117  shortcut_info.extension_id = app_mode::kAppListModeId;
118  shortcut_info.description = shortcut_info.title;
119  shortcut_info.profile_path = profile_path;
120
121  return shortcut_info;
122}
123
124void CreateAppListShim(const base::FilePath& profile_path) {
125  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
126  WebApplicationInfo web_app_info;
127  ShellIntegration::ShortcutInfo shortcut_info =
128      GetAppListShortcutInfo(profile_path);
129
130  ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
131  // TODO(tapted): Add more icon scales when the resource bundle will use them
132  // properly. See http://crbug.com/167408 and http://crbug.com/241304 .
133  shortcut_info.favicon.Add(
134      *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128));
135
136  // TODO(tapted): Create a dock icon using chrome/browser/mac/dock.h .
137  web_app::CreateShortcuts(shortcut_info,
138                           ShellIntegration::ShortcutLocations());
139}
140
141// Check that there is an app list shim. If enabling and there is not, make one.
142// If disabling with --enable-app-list-shim=0, and there is one, delete it.
143void CheckAppListShimOnFileThread(const base::FilePath& profile_path) {
144  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
145  const bool enable =
146      CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppListShim);
147  base::FilePath install_path = web_app::GetAppInstallPath(
148      GetAppListShortcutInfo(profile_path));
149  if (enable == file_util::PathExists(install_path))
150    return;
151
152  if (enable) {
153    content::BrowserThread::PostTask(
154        content::BrowserThread::UI, FROM_HERE,
155        base::Bind(&CreateAppListShim, profile_path));
156    return;
157  }
158
159  // Sanity check because deleting things recursively is scary.
160  CHECK(install_path.MatchesExtension(".app"));
161  file_util::Delete(install_path, true /* recursive */);
162}
163
164AppListControllerDelegateCocoa::AppListControllerDelegateCocoa() {}
165
166AppListControllerDelegateCocoa::~AppListControllerDelegateCocoa() {}
167
168void AppListControllerDelegateCocoa::DismissView() {
169  AppListServiceMac::GetInstance()->DismissAppList();
170}
171
172gfx::NativeWindow AppListControllerDelegateCocoa::GetAppListWindow() {
173  return AppListServiceMac::GetInstance()->GetAppListWindow();
174}
175
176bool AppListControllerDelegateCocoa::CanPin() {
177  return false;
178}
179
180bool AppListControllerDelegateCocoa::CanShowCreateShortcutsDialog() {
181  // TODO(tapted): Return true when create shortcuts menu is tested on mac.
182  return false;
183}
184
185void AppListControllerDelegateCocoa::ActivateApp(
186    Profile* profile, const extensions::Extension* extension, int event_flags) {
187  LaunchApp(profile, extension, event_flags);
188}
189
190void AppListControllerDelegateCocoa::LaunchApp(
191    Profile* profile, const extensions::Extension* extension, int event_flags) {
192  chrome::OpenApplication(chrome::AppLaunchParams(
193      profile, extension, NEW_FOREGROUND_TAB));
194}
195
196void AppListServiceMac::CreateAppList(Profile* requested_profile) {
197  if (profile() == requested_profile)
198    return;
199
200  // The Objective C objects might be released at some unknown point in the
201  // future, so explicitly clear references to C++ objects.
202  [[window_controller_ appListViewController]
203      setDelegate:scoped_ptr<app_list::AppListViewDelegate>(NULL)];
204
205  SetProfile(requested_profile);
206  scoped_ptr<app_list::AppListViewDelegate> delegate(
207      new AppListViewDelegate(new AppListControllerDelegateCocoa(), profile()));
208  window_controller_.reset([[AppListWindowController alloc] init]);
209  [[window_controller_ appListViewController] setDelegate:delegate.Pass()];
210}
211
212void AppListServiceMac::Init(Profile* initial_profile) {
213  // On Mac, Init() is called multiple times for a process: any time there is no
214  // browser window open and a new window is opened, and during process startup
215  // to handle the silent launch case (e.g. for app shims). In the startup case,
216  // a profile has not yet been determined so |initial_profile| will be NULL.
217  if (initial_profile) {
218    static bool checked_shim = false;
219    if (!checked_shim) {
220      checked_shim = true;
221      content::BrowserThread::PostTask(
222          content::BrowserThread::FILE, FROM_HERE,
223          base::Bind(&CheckAppListShimOnFileThread,
224                     initial_profile->GetPath()));
225    }
226  }
227
228  static bool init_called = false;
229  if (init_called)
230    return;
231
232  init_called = true;
233  apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId,
234                                        AppListServiceMac::GetInstance());
235}
236
237void AppListServiceMac::ShowAppList(Profile* requested_profile) {
238  InvalidatePendingProfileLoads();
239
240  if (IsAppListVisible() && (requested_profile == profile())) {
241    ShowWindowNearDock();
242    return;
243  }
244
245  SaveProfilePathToLocalState(requested_profile->GetPath());
246
247  DismissAppList();
248  CreateAppList(requested_profile);
249  ShowWindowNearDock();
250}
251
252void AppListServiceMac::DismissAppList() {
253  if (!IsAppListVisible())
254    return;
255
256  [[window_controller_ window] close];
257
258  FOR_EACH_OBSERVER(apps::AppShimHandler::Host,
259                    observers_,
260                    OnAppClosed());
261}
262
263bool AppListServiceMac::IsAppListVisible() const {
264  return [[window_controller_ window] isVisible];
265}
266
267void AppListServiceMac::EnableAppList() {
268  // TODO(tapted): Implement enable logic here for OSX.
269}
270
271NSWindow* AppListServiceMac::GetAppListWindow() {
272  return [window_controller_ window];
273}
274
275bool AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host,
276                                     apps::AppShimLaunchType launch_type) {
277  ShowForSavedProfile();
278  observers_.AddObserver(host);
279  return true;
280}
281
282void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {
283  observers_.RemoveObserver(host);
284  DismissAppList();
285}
286
287void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host) {
288  DismissAppList();
289}
290
291void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {
292  DismissAppList();
293}
294
295enum DockLocation {
296  DockLocationOtherDisplay,
297  DockLocationBottom,
298  DockLocationLeft,
299  DockLocationRight,
300};
301
302DockLocation DockLocationInDisplay(const gfx::Display& display) {
303  // Assume the dock occupies part of the work area either on the left, right or
304  // bottom of the display. Note in the autohide case, it is always 4 pixels.
305  const gfx::Rect work_area = display.work_area();
306  const gfx::Rect display_bounds = display.bounds();
307  if (work_area.bottom() != display_bounds.bottom())
308    return DockLocationBottom;
309
310  if (work_area.x() != display_bounds.x())
311    return DockLocationLeft;
312
313  if (work_area.right() != display_bounds.right())
314    return DockLocationRight;
315
316  return DockLocationOtherDisplay;
317}
318
319NSPoint GetAppListWindowOrigin(NSWindow* window) {
320  gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]);
321  gfx::Point anchor = screen->GetCursorScreenPoint();
322  const gfx::Display display = screen->GetDisplayNearestPoint(anchor);
323  const DockLocation dock_location = DockLocationInDisplay(display);
324  const gfx::Rect display_bounds = display.bounds();
325
326  // Ensure y coordinates are flipped back into AppKit's coordinate system.
327  const CGFloat max_y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]);
328  if (dock_location == DockLocationOtherDisplay) {
329    // Just display at the bottom-left of the display the cursor is on.
330    return NSMakePoint(display_bounds.x(),
331                       max_y - display_bounds.bottom());
332  }
333
334  // Anchor the center of the window in a region that prevents the window
335  // showing outside of the work area.
336  const NSSize window_size = [window frame].size;
337  gfx::Rect anchor_area = display.work_area();
338  anchor_area.Inset(window_size.width / 2, window_size.height / 2);
339  anchor.SetToMax(anchor_area.origin());
340  anchor.SetToMin(anchor_area.bottom_right());
341
342  // Move anchor to the dock, keeping the other axis aligned with the cursor.
343  switch (dock_location) {
344    case DockLocationBottom:
345      anchor.set_y(anchor_area.bottom());
346      break;
347    case DockLocationLeft:
348      anchor.set_x(anchor_area.x());
349      break;
350    case DockLocationRight:
351      anchor.set_x(anchor_area.right());
352      break;
353    default:
354      NOTREACHED();
355  }
356
357  return NSMakePoint(
358      anchor.x() - window_size.width / 2,
359      max_y - anchor.y() - window_size.height / 2);
360}
361
362void AppListServiceMac::ShowWindowNearDock() {
363  NSWindow* window = GetAppListWindow();
364  DCHECK(window);
365  [window setFrameOrigin:GetAppListWindowOrigin(window)];
366  [window makeKeyAndOrderFront:nil];
367  [NSApp activateIgnoringOtherApps:YES];
368}
369
370}  // namespace
371
372// static
373AppListService* AppListService::Get() {
374  return AppListServiceMac::GetInstance();
375}
376
377// static
378void AppListService::InitAll(Profile* initial_profile) {
379  Get()->Init(initial_profile);
380}
381