app_list_service_mac.mm revision 7d4cd473f85ac64c3747c96c277f9e506a0d2246
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  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
132  if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
133#if defined(GOOGLE_CHROME_BUILD)
134    shortcut_info.favicon.Add(
135        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_16));
136    shortcut_info.favicon.Add(
137        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_32));
138    shortcut_info.favicon.Add(
139        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_128));
140    shortcut_info.favicon.Add(
141        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_256));
142#else
143    NOTREACHED();
144#endif
145  } else {
146    shortcut_info.favicon.Add(
147        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16));
148    shortcut_info.favicon.Add(
149        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32));
150    shortcut_info.favicon.Add(
151        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128));
152    shortcut_info.favicon.Add(
153        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256));
154  }
155
156  // TODO(tapted): Create a dock icon using chrome/browser/mac/dock.h .
157  web_app::CreateShortcuts(shortcut_info,
158                           ShellIntegration::ShortcutLocations());
159}
160
161// Check that there is an app list shim. If enabling and there is not, make one.
162// If disabling with --enable-app-list-shim=0, and there is one, delete it.
163void CheckAppListShimOnFileThread(const base::FilePath& profile_path) {
164  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
165  const bool enable =
166      CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppListShim);
167  base::FilePath install_path = web_app::GetAppInstallPath(
168      GetAppListShortcutInfo(profile_path));
169  if (enable == file_util::PathExists(install_path))
170    return;
171
172  if (enable) {
173    content::BrowserThread::PostTask(
174        content::BrowserThread::UI, FROM_HERE,
175        base::Bind(&CreateAppListShim, profile_path));
176    return;
177  }
178
179  // Sanity check because deleting things recursively is scary.
180  CHECK(install_path.MatchesExtension(".app"));
181  file_util::Delete(install_path, true /* recursive */);
182}
183
184AppListControllerDelegateCocoa::AppListControllerDelegateCocoa() {}
185
186AppListControllerDelegateCocoa::~AppListControllerDelegateCocoa() {}
187
188void AppListControllerDelegateCocoa::DismissView() {
189  AppListServiceMac::GetInstance()->DismissAppList();
190}
191
192gfx::NativeWindow AppListControllerDelegateCocoa::GetAppListWindow() {
193  return AppListServiceMac::GetInstance()->GetAppListWindow();
194}
195
196bool AppListControllerDelegateCocoa::CanPin() {
197  return false;
198}
199
200bool AppListControllerDelegateCocoa::CanShowCreateShortcutsDialog() {
201  // TODO(tapted): Return true when create shortcuts menu is tested on mac.
202  return false;
203}
204
205void AppListControllerDelegateCocoa::ActivateApp(
206    Profile* profile, const extensions::Extension* extension, int event_flags) {
207  LaunchApp(profile, extension, event_flags);
208}
209
210void AppListControllerDelegateCocoa::LaunchApp(
211    Profile* profile, const extensions::Extension* extension, int event_flags) {
212  chrome::OpenApplication(chrome::AppLaunchParams(
213      profile, extension, NEW_FOREGROUND_TAB));
214}
215
216void AppListServiceMac::CreateAppList(Profile* requested_profile) {
217  if (profile() == requested_profile)
218    return;
219
220  // The Objective C objects might be released at some unknown point in the
221  // future, so explicitly clear references to C++ objects.
222  [[window_controller_ appListViewController]
223      setDelegate:scoped_ptr<app_list::AppListViewDelegate>(NULL)];
224
225  SetProfile(requested_profile);
226  scoped_ptr<app_list::AppListViewDelegate> delegate(
227      new AppListViewDelegate(new AppListControllerDelegateCocoa(), profile()));
228  window_controller_.reset([[AppListWindowController alloc] init]);
229  [[window_controller_ appListViewController] setDelegate:delegate.Pass()];
230}
231
232void AppListServiceMac::Init(Profile* initial_profile) {
233  // On Mac, Init() is called multiple times for a process: any time there is no
234  // browser window open and a new window is opened, and during process startup
235  // to handle the silent launch case (e.g. for app shims). In the startup case,
236  // a profile has not yet been determined so |initial_profile| will be NULL.
237  if (initial_profile) {
238    static bool checked_shim = false;
239    if (!checked_shim) {
240      checked_shim = true;
241      content::BrowserThread::PostTask(
242          content::BrowserThread::FILE, FROM_HERE,
243          base::Bind(&CheckAppListShimOnFileThread,
244                     initial_profile->GetPath()));
245    }
246  }
247
248  static bool init_called = false;
249  if (init_called)
250    return;
251
252  init_called = true;
253  apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId,
254                                        AppListServiceMac::GetInstance());
255}
256
257void AppListServiceMac::ShowAppList(Profile* requested_profile) {
258  InvalidatePendingProfileLoads();
259
260  if (IsAppListVisible() && (requested_profile == profile())) {
261    ShowWindowNearDock();
262    return;
263  }
264
265  SaveProfilePathToLocalState(requested_profile->GetPath());
266
267  DismissAppList();
268  CreateAppList(requested_profile);
269  ShowWindowNearDock();
270}
271
272void AppListServiceMac::DismissAppList() {
273  if (!IsAppListVisible())
274    return;
275
276  [[window_controller_ window] close];
277
278  FOR_EACH_OBSERVER(apps::AppShimHandler::Host,
279                    observers_,
280                    OnAppClosed());
281}
282
283bool AppListServiceMac::IsAppListVisible() const {
284  return [[window_controller_ window] isVisible];
285}
286
287void AppListServiceMac::EnableAppList() {
288  // TODO(tapted): Implement enable logic here for OSX.
289}
290
291NSWindow* AppListServiceMac::GetAppListWindow() {
292  return [window_controller_ window];
293}
294
295bool AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host,
296                                     apps::AppShimLaunchType launch_type) {
297  ShowForSavedProfile();
298  observers_.AddObserver(host);
299  return true;
300}
301
302void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {
303  observers_.RemoveObserver(host);
304  DismissAppList();
305}
306
307void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host) {
308  DismissAppList();
309}
310
311void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {
312  DismissAppList();
313}
314
315enum DockLocation {
316  DockLocationOtherDisplay,
317  DockLocationBottom,
318  DockLocationLeft,
319  DockLocationRight,
320};
321
322DockLocation DockLocationInDisplay(const gfx::Display& display) {
323  // Assume the dock occupies part of the work area either on the left, right or
324  // bottom of the display. Note in the autohide case, it is always 4 pixels.
325  const gfx::Rect work_area = display.work_area();
326  const gfx::Rect display_bounds = display.bounds();
327  if (work_area.bottom() != display_bounds.bottom())
328    return DockLocationBottom;
329
330  if (work_area.x() != display_bounds.x())
331    return DockLocationLeft;
332
333  if (work_area.right() != display_bounds.right())
334    return DockLocationRight;
335
336  return DockLocationOtherDisplay;
337}
338
339NSPoint GetAppListWindowOrigin(NSWindow* window) {
340  gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]);
341  gfx::Point anchor = screen->GetCursorScreenPoint();
342  const gfx::Display display = screen->GetDisplayNearestPoint(anchor);
343  const DockLocation dock_location = DockLocationInDisplay(display);
344  const gfx::Rect display_bounds = display.bounds();
345
346  // Ensure y coordinates are flipped back into AppKit's coordinate system.
347  const CGFloat max_y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]);
348  if (dock_location == DockLocationOtherDisplay) {
349    // Just display at the bottom-left of the display the cursor is on.
350    return NSMakePoint(display_bounds.x(),
351                       max_y - display_bounds.bottom());
352  }
353
354  // Anchor the center of the window in a region that prevents the window
355  // showing outside of the work area.
356  const NSSize window_size = [window frame].size;
357  gfx::Rect anchor_area = display.work_area();
358  anchor_area.Inset(window_size.width / 2, window_size.height / 2);
359  anchor.SetToMax(anchor_area.origin());
360  anchor.SetToMin(anchor_area.bottom_right());
361
362  // Move anchor to the dock, keeping the other axis aligned with the cursor.
363  switch (dock_location) {
364    case DockLocationBottom:
365      anchor.set_y(anchor_area.bottom());
366      break;
367    case DockLocationLeft:
368      anchor.set_x(anchor_area.x());
369      break;
370    case DockLocationRight:
371      anchor.set_x(anchor_area.right());
372      break;
373    default:
374      NOTREACHED();
375  }
376
377  return NSMakePoint(
378      anchor.x() - window_size.width / 2,
379      max_y - anchor.y() - window_size.height / 2);
380}
381
382void AppListServiceMac::ShowWindowNearDock() {
383  NSWindow* window = GetAppListWindow();
384  DCHECK(window);
385  [window setFrameOrigin:GetAppListWindowOrigin(window)];
386  [window makeKeyAndOrderFront:nil];
387  [NSApp activateIgnoringOtherApps:YES];
388}
389
390}  // namespace
391
392// static
393AppListService* AppListService::Get() {
394  return AppListServiceMac::GetInstance();
395}
396
397// static
398void AppListService::InitAll(Profile* initial_profile) {
399  Get()->Init(initial_profile);
400}
401