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