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