app_list_service_mac.mm revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
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#import "chrome/browser/ui/app_list/app_list_service_mac.h"
6
7#include <ApplicationServices/ApplicationServices.h>
8#import <Cocoa/Cocoa.h>
9
10#include "apps/app_launcher.h"
11#include "apps/app_shim/app_shim_mac.h"
12#include "apps/pref_names.h"
13#include "base/bind.h"
14#include "base/command_line.h"
15#include "base/file_util.h"
16#include "base/lazy_instance.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_version_info.h"
33#include "chrome/common/mac/app_mode_common.h"
34#include "content/public/browser/browser_thread.h"
35#include "grit/chrome_unscaled_resources.h"
36#include "grit/google_chrome_strings.h"
37#import "ui/app_list/cocoa/app_list_view_controller.h"
38#import "ui/app_list/cocoa/app_list_window_controller.h"
39#include "ui/base/l10n/l10n_util.h"
40#include "ui/base/resource/resource_bundle.h"
41#include "ui/gfx/display.h"
42#include "ui/gfx/screen.h"
43
44namespace gfx {
45class ImageSkia;
46}
47
48// Controller for animations that show or hide the app list.
49@interface AppListAnimationController : NSObject<NSAnimationDelegate> {
50 @private
51  // When closing, the window to close. Retained until the animation ends.
52  base::scoped_nsobject<NSWindow> window_;
53  // The animation started and owned by |self|. Reset when the animation ends.
54  base::scoped_nsobject<NSViewAnimation> animation_;
55}
56
57// Returns whether |window_| is scheduled to be closed when the animation ends.
58- (BOOL)isClosing;
59
60// Animate |window| to show or close it, after cancelling any current animation.
61// Translates from the current location to |targetOrigin| and fades in or out.
62- (void)animateWindow:(NSWindow*)window
63         targetOrigin:(NSPoint)targetOrigin
64              closing:(BOOL)closing;
65
66@end
67
68namespace {
69
70// Version of the app list shortcut version installed.
71const int kShortcutVersion = 1;
72
73// Duration of show and hide animations.
74const NSTimeInterval kAnimationDuration = 0.2;
75
76// Distance towards the screen edge that the app list moves from when showing.
77const CGFloat kDistanceMovedOnShow = 20;
78
79class AppListControllerDelegateCocoa : public AppListControllerDelegate {
80 public:
81  AppListControllerDelegateCocoa();
82  virtual ~AppListControllerDelegateCocoa();
83
84 private:
85  // AppListControllerDelegate overrides:
86  virtual void DismissView() OVERRIDE;
87  virtual gfx::NativeWindow GetAppListWindow() OVERRIDE;
88  virtual bool CanPin() OVERRIDE;
89  virtual bool CanDoCreateShortcutsFlow(bool is_platform_app) OVERRIDE;
90  virtual void CreateNewWindow(Profile* profile, bool incognito) OVERRIDE;
91  virtual void DoCreateShortcutsFlow(Profile* profile,
92                                     const std::string& extension_id) OVERRIDE;
93  virtual void ActivateApp(Profile* profile,
94                           const extensions::Extension* extension,
95                           int event_flags) OVERRIDE;
96  virtual void LaunchApp(Profile* profile,
97                         const extensions::Extension* extension,
98                         int event_flags) OVERRIDE;
99
100  DISALLOW_COPY_AND_ASSIGN(AppListControllerDelegateCocoa);
101};
102
103ShellIntegration::ShortcutInfo GetAppListShortcutInfo(
104    const base::FilePath& profile_path) {
105  ShellIntegration::ShortcutInfo shortcut_info;
106  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
107  if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
108    shortcut_info.title =
109        l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME_CANARY);
110  } else {
111    shortcut_info.title = l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME);
112  }
113
114  shortcut_info.extension_id = app_mode::kAppListModeId;
115  shortcut_info.description = shortcut_info.title;
116  shortcut_info.profile_path = profile_path;
117
118  return shortcut_info;
119}
120
121void CreateAppListShim(const base::FilePath& profile_path) {
122  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
123  WebApplicationInfo web_app_info;
124  ShellIntegration::ShortcutInfo shortcut_info =
125      GetAppListShortcutInfo(profile_path);
126
127  ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
128  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
129  if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
130#if defined(GOOGLE_CHROME_BUILD)
131    shortcut_info.favicon.Add(
132        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_16));
133    shortcut_info.favicon.Add(
134        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_32));
135    shortcut_info.favicon.Add(
136        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_128));
137    shortcut_info.favicon.Add(
138        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_256));
139#else
140    NOTREACHED();
141#endif
142  } else {
143    shortcut_info.favicon.Add(
144        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16));
145    shortcut_info.favicon.Add(
146        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32));
147    shortcut_info.favicon.Add(
148        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128));
149    shortcut_info.favicon.Add(
150        *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256));
151  }
152
153  ShellIntegration::ShortcutLocations shortcut_locations;
154  PrefService* local_state = g_browser_process->local_state();
155  int installed_version =
156      local_state->GetInteger(apps::prefs::kAppLauncherShortcutVersion);
157
158  // If this is a first-time install, add a dock icon. Otherwise just update
159  // the target, and wait for OSX to refresh its icon caches. This might not
160  // occur until a reboot, but OSX does not offer a nicer way. Deleting cache
161  // files on disk and killing processes can easily result in icon corruption.
162  if (installed_version == 0)
163    shortcut_locations.in_quick_launch_bar = true;
164
165  web_app::CreateShortcuts(shortcut_info,
166                           shortcut_locations,
167                           web_app::SHORTCUT_CREATION_AUTOMATED);
168
169  local_state->SetInteger(apps::prefs::kAppLauncherShortcutVersion,
170                          kShortcutVersion);
171}
172
173void CreateShortcutsInDefaultLocation(
174    const ShellIntegration::ShortcutInfo& shortcut_info) {
175  web_app::CreateShortcuts(shortcut_info,
176                           ShellIntegration::ShortcutLocations(),
177                           web_app::SHORTCUT_CREATION_BY_USER);
178}
179
180NSRunningApplication* ActiveApplicationNotChrome() {
181  NSArray* applications = [[NSWorkspace sharedWorkspace] runningApplications];
182  for (NSRunningApplication* application in applications) {
183    if (![application isActive])
184      continue;
185
186    if ([application isEqual:[NSRunningApplication currentApplication]])
187      return nil;  // Chrome is active.
188
189    return application;
190  }
191
192  return nil;
193}
194
195AppListControllerDelegateCocoa::AppListControllerDelegateCocoa() {}
196
197AppListControllerDelegateCocoa::~AppListControllerDelegateCocoa() {}
198
199void AppListControllerDelegateCocoa::DismissView() {
200  AppListServiceMac::GetInstance()->DismissAppList();
201}
202
203gfx::NativeWindow AppListControllerDelegateCocoa::GetAppListWindow() {
204  return AppListServiceMac::GetInstance()->GetAppListWindow();
205}
206
207bool AppListControllerDelegateCocoa::CanPin() {
208  return false;
209}
210
211bool AppListControllerDelegateCocoa::CanDoCreateShortcutsFlow(
212    bool is_platform_app) {
213  return false;
214}
215
216void AppListControllerDelegateCocoa::DoCreateShortcutsFlow(
217    Profile* profile, const std::string& extension_id) {
218  ExtensionService* service =
219      extensions::ExtensionSystem::Get(profile)->extension_service();
220  DCHECK(service);
221  const extensions::Extension* extension =
222      service->GetInstalledExtension(extension_id);
223  DCHECK(extension);
224
225  web_app::UpdateShortcutInfoAndIconForApp(
226      *extension, profile, base::Bind(&CreateShortcutsInDefaultLocation));
227}
228
229void AppListControllerDelegateCocoa::CreateNewWindow(
230    Profile* profile, bool incognito) {
231  Profile* window_profile = incognito ?
232      profile->GetOffTheRecordProfile() : profile;
233  chrome::NewEmptyWindow(window_profile, chrome::GetActiveDesktop());
234}
235
236void AppListControllerDelegateCocoa::ActivateApp(
237    Profile* profile, const extensions::Extension* extension, int event_flags) {
238  LaunchApp(profile, extension, event_flags);
239}
240
241void AppListControllerDelegateCocoa::LaunchApp(
242    Profile* profile, const extensions::Extension* extension, int event_flags) {
243  chrome::OpenApplication(chrome::AppLaunchParams(
244      profile, extension, NEW_FOREGROUND_TAB));
245}
246
247enum DockLocation {
248  DockLocationOtherDisplay,
249  DockLocationBottom,
250  DockLocationLeft,
251  DockLocationRight,
252};
253
254DockLocation DockLocationInDisplay(const gfx::Display& display) {
255  // Assume the dock occupies part of the work area either on the left, right or
256  // bottom of the display. Note in the autohide case, it is always 4 pixels.
257  const gfx::Rect work_area = display.work_area();
258  const gfx::Rect display_bounds = display.bounds();
259  if (work_area.bottom() != display_bounds.bottom())
260    return DockLocationBottom;
261
262  if (work_area.x() != display_bounds.x())
263    return DockLocationLeft;
264
265  if (work_area.right() != display_bounds.right())
266    return DockLocationRight;
267
268  return DockLocationOtherDisplay;
269}
270
271// If |work_area_edge| is too close to the |screen_edge| (e.g. autohide dock),
272// adjust |anchor| away from the edge by a constant amount to reduce overlap and
273// ensure the dock icon can still be clicked to dismiss the app list.
274int AdjustPointForDynamicDock(int anchor, int screen_edge, int work_area_edge) {
275  const int kAutohideDockThreshold = 10;
276  const int kExtraDistance = 50;  // A dock with 40 items is about this size.
277  if (abs(work_area_edge - screen_edge) > kAutohideDockThreshold)
278    return anchor;
279
280  return anchor +
281      (screen_edge < work_area_edge ? kExtraDistance : -kExtraDistance);
282}
283
284void GetAppListWindowOrigins(
285    NSWindow* window, NSPoint* target_origin, NSPoint* start_origin) {
286  gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]);
287  // Ensure y coordinates are flipped back into AppKit's coordinate system.
288  const CGFloat max_y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]);
289  if (!CGCursorIsVisible()) {
290    // If Chrome is the active application, display on the same display as
291    // Chrome's keyWindow since this will catch activations triggered, e.g, via
292    // WebStore install. If another application is active, OSX doesn't provide a
293    // reliable way to get the display in use. Fall back to the primary display
294    // since it has the menu bar and is likely to be correct, e.g., for
295    // activations from Spotlight.
296    const gfx::NativeView key_view = [[NSApp keyWindow] contentView];
297    const gfx::Rect work_area = key_view && [NSApp isActive] ?
298        screen->GetDisplayNearestWindow(key_view).work_area() :
299        screen->GetPrimaryDisplay().work_area();
300    *target_origin = NSMakePoint(work_area.x(), max_y - work_area.bottom());
301    *start_origin = *target_origin;
302    return;
303  }
304
305  gfx::Point anchor = screen->GetCursorScreenPoint();
306  const gfx::Display display = screen->GetDisplayNearestPoint(anchor);
307  const DockLocation dock_location = DockLocationInDisplay(display);
308  const gfx::Rect display_bounds = display.bounds();
309
310  if (dock_location == DockLocationOtherDisplay) {
311    // Just display at the bottom-left of the display the cursor is on.
312    *target_origin = NSMakePoint(display_bounds.x(),
313                                 max_y - display_bounds.bottom());
314    *start_origin = *target_origin;
315    return;
316  }
317
318  // Anchor the center of the window in a region that prevents the window
319  // showing outside of the work area.
320  const NSSize window_size = [window frame].size;
321  const gfx::Rect work_area = display.work_area();
322  gfx::Rect anchor_area = work_area;
323  anchor_area.Inset(window_size.width / 2, window_size.height / 2);
324  anchor.SetToMax(anchor_area.origin());
325  anchor.SetToMin(anchor_area.bottom_right());
326
327  // Move anchor to the dock, keeping the other axis aligned with the cursor.
328  switch (dock_location) {
329    case DockLocationBottom:
330      anchor.set_y(AdjustPointForDynamicDock(
331          anchor_area.bottom(), display_bounds.bottom(), work_area.bottom()));
332      break;
333    case DockLocationLeft:
334      anchor.set_x(AdjustPointForDynamicDock(
335          anchor_area.x(), display_bounds.x(), work_area.x()));
336      break;
337    case DockLocationRight:
338      anchor.set_x(AdjustPointForDynamicDock(
339          anchor_area.right(), display_bounds.right(), work_area.right()));
340      break;
341    default:
342      NOTREACHED();
343  }
344
345  *target_origin = NSMakePoint(anchor.x() - window_size.width / 2,
346                               max_y - anchor.y() - window_size.height / 2);
347  *start_origin = *target_origin;
348
349  switch (dock_location) {
350    case DockLocationBottom:
351      start_origin->y -= kDistanceMovedOnShow;
352      break;
353    case DockLocationLeft:
354      start_origin->x -= kDistanceMovedOnShow;
355      break;
356    case DockLocationRight:
357      start_origin->x += kDistanceMovedOnShow;
358      break;
359    default:
360      NOTREACHED();
361  }
362}
363
364}  // namespace
365
366AppListServiceMac::AppListServiceMac() {
367  animation_controller_.reset([[AppListAnimationController alloc] init]);
368}
369
370AppListServiceMac::~AppListServiceMac() {}
371
372// static
373AppListServiceMac* AppListServiceMac::GetInstance() {
374  return Singleton<AppListServiceMac,
375                   LeakySingletonTraits<AppListServiceMac> >::get();
376}
377
378void AppListServiceMac::Init(Profile* initial_profile) {
379  // On Mac, Init() is called multiple times for a process: any time there is no
380  // browser window open and a new window is opened, and during process startup
381  // to handle the silent launch case (e.g. for app shims). In the startup case,
382  // a profile has not yet been determined so |initial_profile| will be NULL.
383  static bool init_called_with_profile = false;
384  if (initial_profile && !init_called_with_profile) {
385    init_called_with_profile = true;
386    HandleCommandLineFlags(initial_profile);
387    PrefService* local_state = g_browser_process->local_state();
388    if (!apps::IsAppLauncherEnabled()) {
389      local_state->SetInteger(apps::prefs::kAppLauncherShortcutVersion, 0);
390    } else {
391      int installed_shortcut_version =
392          local_state->GetInteger(apps::prefs::kAppLauncherShortcutVersion);
393
394      if (kShortcutVersion > installed_shortcut_version)
395        CreateShortcut();
396    }
397  }
398
399  static bool init_called = false;
400  if (init_called)
401    return;
402
403  init_called = true;
404  apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId,
405                                        AppListServiceMac::GetInstance());
406}
407
408void AppListServiceMac::CreateForProfile(Profile* requested_profile) {
409  if (profile() == requested_profile)
410    return;
411
412  // The Objective C objects might be released at some unknown point in the
413  // future, so explicitly clear references to C++ objects.
414  [[window_controller_ appListViewController]
415      setDelegate:scoped_ptr<app_list::AppListViewDelegate>()];
416
417  SetProfile(requested_profile);
418  scoped_ptr<app_list::AppListViewDelegate> delegate(
419      new AppListViewDelegate(new AppListControllerDelegateCocoa(), profile()));
420  window_controller_.reset([[AppListWindowController alloc] init]);
421  [[window_controller_ appListViewController] setDelegate:delegate.Pass()];
422}
423
424void AppListServiceMac::ShowForProfile(Profile* requested_profile) {
425  if (requested_profile->IsManaged())
426    return;
427
428  InvalidatePendingProfileLoads();
429
430  if (IsAppListVisible() && (requested_profile == profile())) {
431    ShowWindowNearDock();
432    return;
433  }
434
435  SetProfilePath(requested_profile->GetPath());
436
437  DismissAppList();
438  CreateForProfile(requested_profile);
439  ShowWindowNearDock();
440}
441
442void AppListServiceMac::DismissAppList() {
443  if (!IsAppListVisible())
444    return;
445
446  // If the app list is currently the main window, it will activate the next
447  // Chrome window when dismissed. But if a different application was active
448  // when the app list was shown, activate that instead.
449  base::scoped_nsobject<NSRunningApplication> prior_app;
450  if ([[window_controller_ window] isMainWindow])
451    prior_app.swap(previously_active_application_);
452  else
453    previously_active_application_.reset();
454
455  // If activation is successful, the app list will lose main status and try to
456  // close itself again. It can't be closed in this runloop iteration without
457  // OSX deciding to raise the next Chrome window, and _then_ activating the
458  // application on top. This also occurs if no activation option is given.
459  if ([prior_app activateWithOptions:NSApplicationActivateIgnoringOtherApps])
460    return;
461
462  [animation_controller_ animateWindow:[window_controller_ window]
463                          targetOrigin:last_start_origin_
464                               closing:YES];
465}
466
467bool AppListServiceMac::IsAppListVisible() const {
468  return [[window_controller_ window] isVisible] &&
469      ![animation_controller_ isClosing];
470}
471
472void AppListServiceMac::CreateShortcut() {
473  CreateAppListShim(GetProfilePath(
474      g_browser_process->profile_manager()->user_data_dir()));
475}
476
477NSWindow* AppListServiceMac::GetAppListWindow() {
478  return [window_controller_ window];
479}
480
481AppListControllerDelegate* AppListServiceMac::CreateControllerDelegate() {
482  return new AppListControllerDelegateCocoa();
483}
484
485void AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host,
486                                     apps::AppShimLaunchType launch_type,
487                                     const std::vector<base::FilePath>& files) {
488  if (IsAppListVisible())
489    DismissAppList();
490  else
491    Show();
492
493  // Always close the shim process immediately.
494  host->OnAppLaunchComplete(apps::APP_SHIM_LAUNCH_DUPLICATE_HOST);
495}
496
497void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {}
498
499void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host,
500                                    apps::AppShimFocusType focus_type,
501                                    const std::vector<base::FilePath>& files) {}
502
503void AppListServiceMac::OnShimSetHidden(apps::AppShimHandler::Host* host,
504                                        bool hidden) {}
505
506void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {}
507
508void AppListServiceMac::ShowWindowNearDock() {
509  NSWindow* window = GetAppListWindow();
510  DCHECK(window);
511  NSPoint target_origin;
512  GetAppListWindowOrigins(window, &target_origin, &last_start_origin_);
513  [window setFrameOrigin:last_start_origin_];
514
515  // Before activating, see if an application other than Chrome is currently the
516  // active application, so that it can be reactivated when dismissing.
517  previously_active_application_.reset([ActiveApplicationNotChrome() retain]);
518
519  [animation_controller_ animateWindow:[window_controller_ window]
520                          targetOrigin:target_origin
521                               closing:NO];
522  [window makeKeyAndOrderFront:nil];
523  [NSApp activateIgnoringOtherApps:YES];
524}
525
526// static
527AppListService* AppListService::Get() {
528  return AppListServiceMac::GetInstance();
529}
530
531// static
532void AppListService::InitAll(Profile* initial_profile) {
533  Get()->Init(initial_profile);
534}
535
536@implementation AppListAnimationController
537
538- (BOOL)isClosing {
539  return !!window_;
540}
541
542- (void)animateWindow:(NSWindow*)window
543         targetOrigin:(NSPoint)targetOrigin
544              closing:(BOOL)closing {
545  // First, stop the existing animation, if there is one.
546  [animation_ stopAnimation];
547
548  NSRect targetFrame = [window frame];
549  targetFrame.origin = targetOrigin;
550
551  // NSViewAnimation has a quirk when setting the curve to NSAnimationEaseOut
552  // where it attempts to auto-reverse the animation. FadeOut becomes FadeIn
553  // (good), but FrameKey is also switched (bad). So |targetFrame| needs to be
554  // put on the StartFrameKey when using NSAnimationEaseOut for showing.
555  NSArray* animationArray = @[
556    @{
557      NSViewAnimationTargetKey : window,
558      NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect,
559      (closing ? NSViewAnimationEndFrameKey : NSViewAnimationStartFrameKey) :
560          [NSValue valueWithRect:targetFrame]
561    }
562  ];
563  animation_.reset(
564      [[NSViewAnimation alloc] initWithViewAnimations:animationArray]);
565  [animation_ setDuration:kAnimationDuration];
566  [animation_ setDelegate:self];
567
568  if (closing) {
569    [animation_ setAnimationCurve:NSAnimationEaseIn];
570    window_.reset([window retain]);
571  } else {
572    [window setAlphaValue:0.0f];
573    [animation_ setAnimationCurve:NSAnimationEaseOut];
574    window_.reset();
575  }
576  [animation_ startAnimation];
577}
578
579- (void)animationDidEnd:(NSAnimation*)animation {
580  [window_ close];
581  window_.reset();
582  animation_.reset();
583}
584
585@end
586