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