app_list_service_mac.mm revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
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 = DockLocationInDisplay(display);
284
285  gfx::Point anchor;
286  // Snap to the dock edge. If the cursor is greater than the window
287  // width/height away or not visible, anchor to the center of the dock.
288  // Otherwise, anchor to the cursor position.
289  if (dock_location == AppListPositioner::SCREEN_EDGE_UNKNOWN) {
290    anchor = positioner.GetAnchorPointForScreenCorner(
291        AppListPositioner::SCREEN_CORNER_BOTTOM_LEFT);
292  } else {
293    int snap_distance =
294        dock_location == AppListPositioner::SCREEN_EDGE_BOTTOM ||
295                dock_location == AppListPositioner::SCREEN_EDGE_TOP ?
296            window_size.height() :
297            window_size.width();
298    // Subtract the dock area since the display's default work_area will not
299    // subtract it if the dock is set to auto-hide, and the app list should
300    // never overlap the dock.
301    AdjustWorkAreaForDock(display, &positioner, dock_location);
302    if (!cursor_is_visible || positioner.GetCursorDistanceFromShelf(
303                                  dock_location, cursor) > snap_distance) {
304      anchor = positioner.GetAnchorPointForShelfCenter(dock_location);
305    } else {
306      anchor = positioner.GetAnchorPointForShelfCursor(dock_location, cursor);
307    }
308  }
309
310  *target_origin = NSMakePoint(
311      anchor.x() - window_size.width() / 2,
312      primary_display_height - anchor.y() - window_size.height() / 2);
313  *start_origin = *target_origin;
314
315  // If the launcher is anchored to the dock (regardless of whether the cursor
316  // is visible), animate in inwards from the edge of screen
317  switch (dock_location) {
318    case AppListPositioner::SCREEN_EDGE_UNKNOWN:
319      break;
320    case AppListPositioner::SCREEN_EDGE_LEFT:
321      start_origin->x -= kDistanceMovedOnShow;
322      break;
323    case AppListPositioner::SCREEN_EDGE_RIGHT:
324      start_origin->x += kDistanceMovedOnShow;
325      break;
326    case AppListPositioner::SCREEN_EDGE_TOP:
327      NOTREACHED();
328      break;
329    case AppListPositioner::SCREEN_EDGE_BOTTOM:
330      start_origin->y -= kDistanceMovedOnShow;
331      break;
332  }
333}
334
335void AppListServiceMac::Init(Profile* initial_profile) {
336  // On Mac, Init() is called multiple times for a process: any time there is no
337  // browser window open and a new window is opened, and during process startup
338  // to handle the silent launch case (e.g. for app shims). In the startup case,
339  // a profile has not yet been determined so |initial_profile| will be NULL.
340  static bool init_called_with_profile = false;
341  if (initial_profile && !init_called_with_profile) {
342    init_called_with_profile = true;
343    HandleCommandLineFlags(initial_profile);
344    PrefService* local_state = g_browser_process->local_state();
345    if (!IsAppLauncherEnabled()) {
346      local_state->SetInteger(prefs::kAppLauncherShortcutVersion, 0);
347    } else {
348      int installed_shortcut_version =
349          local_state->GetInteger(prefs::kAppLauncherShortcutVersion);
350
351      if (kShortcutVersion > installed_shortcut_version)
352        CreateShortcut();
353    }
354  }
355
356  static bool init_called = false;
357  if (init_called)
358    return;
359
360  init_called = true;
361  apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId,
362                                        AppListServiceMac::GetInstance());
363}
364
365Profile* AppListServiceMac::GetCurrentAppListProfile() {
366  return profile_;
367}
368
369void AppListServiceMac::CreateForProfile(Profile* requested_profile) {
370  if (profile_ == requested_profile)
371    return;
372
373  profile_ = requested_profile;
374
375  if (window_controller_) {
376    // Clear the search box.
377    [[window_controller_ appListViewController] searchBoxModel]
378        ->SetText(base::string16());
379  } else {
380    window_controller_.reset([[AppListWindowController alloc] init]);
381  }
382
383  scoped_ptr<app_list::AppListViewDelegate> delegate(
384      new AppListViewDelegate(profile_, GetControllerDelegate()));
385  [[window_controller_ appListViewController] setDelegate:delegate.Pass()];
386}
387
388void AppListServiceMac::ShowForProfile(Profile* requested_profile) {
389  if (requested_profile->IsManaged())
390    return;
391
392  InvalidatePendingProfileLoads();
393
394  if (requested_profile == profile_) {
395    ShowWindowNearDock();
396    return;
397  }
398
399  SetProfilePath(requested_profile->GetPath());
400  CreateForProfile(requested_profile);
401  ShowWindowNearDock();
402}
403
404void AppListServiceMac::DismissAppList() {
405  if (!IsAppListVisible())
406    return;
407
408  // If the app list is currently the main window, it will activate the next
409  // Chrome window when dismissed. But if a different application was active
410  // when the app list was shown, activate that instead.
411  base::scoped_nsobject<NSRunningApplication> prior_app;
412  if ([[window_controller_ window] isMainWindow])
413    prior_app.swap(previously_active_application_);
414  else
415    previously_active_application_.reset();
416
417  // If activation is successful, the app list will lose main status and try to
418  // close itself again. It can't be closed in this runloop iteration without
419  // OSX deciding to raise the next Chrome window, and _then_ activating the
420  // application on top. This also occurs if no activation option is given.
421  if ([prior_app activateWithOptions:NSApplicationActivateIgnoringOtherApps])
422    return;
423
424  [animation_controller_ animateWindow:[window_controller_ window]
425                          targetOrigin:last_start_origin_
426                               closing:YES];
427}
428
429bool AppListServiceMac::IsAppListVisible() const {
430  return [[window_controller_ window] isVisible] &&
431      ![animation_controller_ isClosing];
432}
433
434void AppListServiceMac::EnableAppList(Profile* initial_profile) {
435  AppListServiceImpl::EnableAppList(initial_profile);
436  AppController* controller = [NSApp delegate];
437  [controller initAppShimMenuController];
438}
439
440void AppListServiceMac::CreateShortcut() {
441  CreateAppListShim(GetProfilePath(
442      g_browser_process->profile_manager()->user_data_dir()));
443}
444
445NSWindow* AppListServiceMac::GetAppListWindow() {
446  return [window_controller_ window];
447}
448
449AppListControllerDelegate* AppListServiceMac::GetControllerDelegate() {
450  return controller_delegate_.get();
451}
452
453void AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host,
454                                     apps::AppShimLaunchType launch_type,
455                                     const std::vector<base::FilePath>& files) {
456  if (IsAppListVisible())
457    DismissAppList();
458  else
459    Show();
460
461  // Always close the shim process immediately.
462  host->OnAppLaunchComplete(apps::APP_SHIM_LAUNCH_DUPLICATE_HOST);
463}
464
465void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {}
466
467void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host,
468                                    apps::AppShimFocusType focus_type,
469                                    const std::vector<base::FilePath>& files) {}
470
471void AppListServiceMac::OnShimSetHidden(apps::AppShimHandler::Host* host,
472                                        bool hidden) {}
473
474void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {}
475
476void AppListServiceMac::ShowWindowNearDock() {
477  if (IsAppListVisible())
478    return;
479
480  NSWindow* window = GetAppListWindow();
481  DCHECK(window);
482  NSPoint target_origin;
483  GetAppListWindowOrigins(window, &target_origin, &last_start_origin_);
484  [window setFrameOrigin:last_start_origin_];
485
486  // Before activating, see if an application other than Chrome is currently the
487  // active application, so that it can be reactivated when dismissing.
488  previously_active_application_.reset([ActiveApplicationNotChrome() retain]);
489
490  [animation_controller_ animateWindow:[window_controller_ window]
491                          targetOrigin:target_origin
492                               closing:NO];
493  [window makeKeyAndOrderFront:nil];
494  [NSApp activateIgnoringOtherApps:YES];
495  RecordAppListLaunch();
496}
497
498// static
499AppListService* AppListService::Get(chrome::HostDesktopType desktop_type) {
500  return AppListServiceMac::GetInstance();
501}
502
503// static
504void AppListService::InitAll(Profile* initial_profile) {
505  AppListServiceMac::GetInstance()->Init(initial_profile);
506}
507
508@implementation AppListAnimationController
509
510- (BOOL)isClosing {
511  return !!window_;
512}
513
514- (void)animateWindow:(NSWindow*)window
515         targetOrigin:(NSPoint)targetOrigin
516              closing:(BOOL)closing {
517  // First, stop the existing animation, if there is one.
518  [animation_ stopAnimation];
519
520  NSRect targetFrame = [window frame];
521  targetFrame.origin = targetOrigin;
522
523  // NSViewAnimation has a quirk when setting the curve to NSAnimationEaseOut
524  // where it attempts to auto-reverse the animation. FadeOut becomes FadeIn
525  // (good), but FrameKey is also switched (bad). So |targetFrame| needs to be
526  // put on the StartFrameKey when using NSAnimationEaseOut for showing.
527  NSArray* animationArray = @[
528    @{
529      NSViewAnimationTargetKey : window,
530      NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect,
531      (closing ? NSViewAnimationEndFrameKey : NSViewAnimationStartFrameKey) :
532          [NSValue valueWithRect:targetFrame]
533    }
534  ];
535  animation_.reset(
536      [[NSViewAnimation alloc] initWithViewAnimations:animationArray]);
537  [animation_ setDuration:kAnimationDuration];
538  [animation_ setDelegate:self];
539
540  if (closing) {
541    [animation_ setAnimationCurve:NSAnimationEaseIn];
542    window_.reset([window retain]);
543  } else {
544    [window setAlphaValue:0.0f];
545    [animation_ setAnimationCurve:NSAnimationEaseOut];
546    window_.reset();
547  }
548  [animation_ startAnimation];
549}
550
551- (void)animationDidEnd:(NSAnimation*)animation {
552  [window_ close];
553  window_.reset();
554  animation_.reset();
555
556  apps::AppShimHandler::MaybeTerminate();
557}
558
559@end
560