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