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.
5 #import "chrome/browser/ui/app_list/app_list_service_mac.h"
7 #include <ApplicationServices/ApplicationServices.h>
8 #import <Cocoa/Cocoa.h>
10 #include "base/bind.h"
11 #include "base/command_line.h"
12 #include "base/files/file_util.h"
13 #include "base/lazy_instance.h"
14 #include "base/memory/singleton.h"
15 #include "base/message_loop/message_loop.h"
16 #include "base/prefs/pref_service.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/profiles/profile_info_cache.h"
21 #include "chrome/browser/profiles/profile_manager.h"
22 #include "chrome/browser/ui/app_list/app_list_positioner.h"
23 #include "chrome/browser/ui/app_list/app_list_service.h"
24 #include "chrome/browser/ui/app_list/app_list_service_cocoa_mac.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/browser_commands.h"
28 #include "chrome/browser/ui/extensions/application_launch.h"
29 #include "chrome/browser/web_applications/web_app.h"
30 #include "chrome/browser/web_applications/web_app_mac.h"
31 #include "chrome/common/channel_info.h"
32 #include "chrome/common/chrome_switches.h"
33 #include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
34 #include "chrome/common/mac/app_mode_common.h"
35 #include "chrome/common/pref_names.h"
36 #include "chrome/grit/google_chrome_strings.h"
37 #include "components/version_info/version_info.h"
38 #include "content/public/browser/browser_thread.h"
39 #include "extensions/browser/extension_system.h"
40 #include "extensions/common/manifest_handlers/file_handler_info.h"
41 #include "grit/chrome_unscaled_resources.h"
42 #include "net/base/url_util.h"
43 #include "ui/app_list/app_list_switches.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"
54 // Controller for animations that show or hide the app list.
55 @interface AppListAnimationController : NSObject<NSAnimationDelegate> {
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_;
63 // Returns whether |window_| is scheduled to be closed when the animation ends.
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;
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
75 - (void)cleanupOnUIThread;
81 // Version of the app list shortcut version installed.
82 const int kShortcutVersion = 2;
84 // Duration of show and hide animations.
85 const NSTimeInterval kAnimationDuration = 0.2;
87 // Distance towards the screen edge that the app list moves from when showing.
88 const CGFloat kDistanceMovedOnShow = 20;
90 scoped_ptr<web_app::ShortcutInfo> GetAppListShortcutInfo(
91 const base::FilePath& profile_path) {
92 scoped_ptr<web_app::ShortcutInfo> shortcut_info(new web_app::ShortcutInfo);
93 version_info::Channel channel = chrome::GetChannel();
94 if (channel == version_info::Channel::CANARY) {
95 shortcut_info->title =
96 l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME_CANARY);
98 shortcut_info->title =
99 l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME);
102 shortcut_info->extension_id = app_mode::kAppListModeId;
103 shortcut_info->description = shortcut_info->title;
104 shortcut_info->profile_path = profile_path;
106 return shortcut_info;
109 void CreateAppListShim(const base::FilePath& profile_path) {
110 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
111 WebApplicationInfo web_app_info;
112 scoped_ptr<web_app::ShortcutInfo> shortcut_info =
113 GetAppListShortcutInfo(profile_path);
115 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
116 version_info::Channel channel = chrome::GetChannel();
117 if (channel == version_info::Channel::CANARY) {
118 #if defined(GOOGLE_CHROME_BUILD)
119 shortcut_info->favicon.Add(
120 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_16));
121 shortcut_info->favicon.Add(
122 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_32));
123 shortcut_info->favicon.Add(
124 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_128));
125 shortcut_info->favicon.Add(
126 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_256));
131 shortcut_info->favicon.Add(
132 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16));
133 shortcut_info->favicon.Add(
134 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32));
135 shortcut_info->favicon.Add(
136 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128));
137 shortcut_info->favicon.Add(
138 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256));
141 web_app::ShortcutLocations shortcut_locations;
142 PrefService* local_state = g_browser_process->local_state();
143 int installed_version =
144 local_state->GetInteger(prefs::kAppLauncherShortcutVersion);
146 // If this is a first-time install, add a dock icon. Otherwise just update
147 // the target, and wait for OSX to refresh its icon caches. This might not
148 // occur until a reboot, but OSX does not offer a nicer way. Deleting cache
149 // files on disk and killing processes can easily result in icon corruption.
150 if (installed_version == 0)
151 shortcut_locations.in_quick_launch_bar = true;
153 web_app::CreateNonAppShortcut(shortcut_locations, shortcut_info.Pass());
155 local_state->SetInteger(prefs::kAppLauncherShortcutVersion,
159 NSRunningApplication* ActiveApplicationNotChrome() {
160 NSArray* applications = [[NSWorkspace sharedWorkspace] runningApplications];
161 for (NSRunningApplication* application in applications) {
162 if (![application isActive])
165 if ([application isEqual:[NSRunningApplication currentApplication]])
166 return nil; // Chrome is active.
174 // Determines which screen edge the dock is aligned to.
175 AppListPositioner::ScreenEdge DockLocationInDisplay(
176 const gfx::Display& display) {
177 // Assume the dock occupies part of the work area either on the left, right or
178 // bottom of the display. Note in the autohide case, it is always 4 pixels.
179 const gfx::Rect work_area = display.work_area();
180 const gfx::Rect display_bounds = display.bounds();
181 if (work_area.bottom() != display_bounds.bottom())
182 return AppListPositioner::SCREEN_EDGE_BOTTOM;
184 if (work_area.x() != display_bounds.x())
185 return AppListPositioner::SCREEN_EDGE_LEFT;
187 if (work_area.right() != display_bounds.right())
188 return AppListPositioner::SCREEN_EDGE_RIGHT;
190 return AppListPositioner::SCREEN_EDGE_UNKNOWN;
193 // If |display|'s work area is too close to its boundary on |dock_edge|, adjust
194 // the work area away from the edge by a constant amount to reduce overlap and
195 // ensure the dock icon can still be clicked to dismiss the app list.
196 void AdjustWorkAreaForDock(const gfx::Display& display,
197 AppListPositioner* positioner,
198 AppListPositioner::ScreenEdge dock_edge) {
199 const int kAutohideDockThreshold = 10;
200 const int kExtraDistance = 50; // A dock with 40 items is about this size.
202 const gfx::Rect work_area = display.work_area();
203 const gfx::Rect display_bounds = display.bounds();
206 case AppListPositioner::SCREEN_EDGE_LEFT:
207 if (work_area.x() - display_bounds.x() <= kAutohideDockThreshold)
208 positioner->WorkAreaInset(kExtraDistance, 0, 0, 0);
210 case AppListPositioner::SCREEN_EDGE_RIGHT:
211 if (display_bounds.right() - work_area.right() <= kAutohideDockThreshold)
212 positioner->WorkAreaInset(0, 0, kExtraDistance, 0);
214 case AppListPositioner::SCREEN_EDGE_BOTTOM:
215 if (display_bounds.bottom() - work_area.bottom() <=
216 kAutohideDockThreshold) {
217 positioner->WorkAreaInset(0, 0, 0, kExtraDistance);
220 case AppListPositioner::SCREEN_EDGE_UNKNOWN:
221 case AppListPositioner::SCREEN_EDGE_TOP:
227 void GetAppListWindowOrigins(
228 NSWindow* window, NSPoint* target_origin, NSPoint* start_origin) {
229 gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]);
230 // Ensure y coordinates are flipped back into AppKit's coordinate system.
231 bool cursor_is_visible = CGCursorIsVisible();
232 gfx::Display display;
234 if (!cursor_is_visible) {
235 // If Chrome is the active application, display on the same display as
236 // Chrome's keyWindow since this will catch activations triggered, e.g, via
237 // WebStore install. If another application is active, OSX doesn't provide a
238 // reliable way to get the display in use. Fall back to the primary display
239 // since it has the menu bar and is likely to be correct, e.g., for
240 // activations from Spotlight.
241 const gfx::NativeView key_view = [[NSApp keyWindow] contentView];
242 display = key_view && [NSApp isActive] ?
243 screen->GetDisplayNearestWindow(key_view) :
244 screen->GetPrimaryDisplay();
246 cursor = screen->GetCursorScreenPoint();
247 display = screen->GetDisplayNearestPoint(cursor);
250 const NSSize ns_window_size = [window frame].size;
251 gfx::Size window_size(ns_window_size.width, ns_window_size.height);
252 int primary_display_height =
253 NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]);
254 AppListServiceMac::FindAnchorPoint(window_size,
256 primary_display_height,
263 AppListServiceMac* GetActiveInstance() {
264 if (app_list::switches::IsMacViewsAppListEnabled()) {
265 #if defined(TOOLKIT_VIEWS)
266 // TODO(tapted): Return AppListServiceViewsMac instance.
271 return AppListServiceCocoaMac::GetInstance();
276 AppListServiceMac::AppListServiceMac() {
277 animation_controller_.reset([[AppListAnimationController alloc] init]);
280 AppListServiceMac::~AppListServiceMac() {}
283 void AppListServiceMac::FindAnchorPoint(const gfx::Size& window_size,
284 const gfx::Display& display,
285 int primary_display_height,
286 bool cursor_is_visible,
287 const gfx::Point& cursor,
288 NSPoint* target_origin,
289 NSPoint* start_origin) {
290 AppListPositioner positioner(display, window_size, 0);
291 AppListPositioner::ScreenEdge dock_location = DockLocationInDisplay(display);
294 // Snap to the dock edge. If the cursor is greater than the window
295 // width/height away or not visible, anchor to the center of the dock.
296 // Otherwise, anchor to the cursor position.
297 if (dock_location == AppListPositioner::SCREEN_EDGE_UNKNOWN) {
298 anchor = positioner.GetAnchorPointForScreenCorner(
299 AppListPositioner::SCREEN_CORNER_BOTTOM_LEFT);
302 dock_location == AppListPositioner::SCREEN_EDGE_BOTTOM ||
303 dock_location == AppListPositioner::SCREEN_EDGE_TOP ?
304 window_size.height() :
306 // Subtract the dock area since the display's default work_area will not
307 // subtract it if the dock is set to auto-hide, and the app list should
308 // never overlap the dock.
309 AdjustWorkAreaForDock(display, &positioner, dock_location);
310 if (!cursor_is_visible || positioner.GetCursorDistanceFromShelf(
311 dock_location, cursor) > snap_distance) {
312 anchor = positioner.GetAnchorPointForShelfCenter(dock_location);
314 anchor = positioner.GetAnchorPointForShelfCursor(dock_location, cursor);
318 *target_origin = NSMakePoint(
319 anchor.x() - window_size.width() / 2,
320 primary_display_height - anchor.y() - window_size.height() / 2);
321 *start_origin = *target_origin;
323 // If the launcher is anchored to the dock (regardless of whether the cursor
324 // is visible), animate in inwards from the edge of screen
325 switch (dock_location) {
326 case AppListPositioner::SCREEN_EDGE_UNKNOWN:
328 case AppListPositioner::SCREEN_EDGE_LEFT:
329 start_origin->x -= kDistanceMovedOnShow;
331 case AppListPositioner::SCREEN_EDGE_RIGHT:
332 start_origin->x += kDistanceMovedOnShow;
334 case AppListPositioner::SCREEN_EDGE_TOP:
337 case AppListPositioner::SCREEN_EDGE_BOTTOM:
338 start_origin->y -= kDistanceMovedOnShow;
343 void AppListServiceMac::Init(Profile* initial_profile) {
344 InitWithProfilePath(initial_profile, initial_profile->GetPath());
347 void AppListServiceMac::InitWithProfilePath(
348 Profile* initial_profile,
349 const base::FilePath& profile_path) {
350 // On Mac, Init() is called multiple times for a process: any time there is no
351 // browser window open and a new window is opened, and during process startup
352 // to handle the silent launch case (e.g. for app shims). In the startup case,
353 // a profile has not yet been determined so |initial_profile| will be NULL.
354 static bool init_called_with_profile = false;
355 if (initial_profile && !init_called_with_profile) {
356 init_called_with_profile = true;
357 PerformStartupChecks(initial_profile);
358 PrefService* local_state = g_browser_process->local_state();
359 if (!IsAppLauncherEnabled()) {
360 local_state->SetInteger(prefs::kAppLauncherShortcutVersion, 0);
362 int installed_shortcut_version =
363 local_state->GetInteger(prefs::kAppLauncherShortcutVersion);
365 if (kShortcutVersion > installed_shortcut_version)
370 static bool init_called = false;
375 apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId, this);
377 // Handle the case where Chrome was not running and was started with the app
378 // launcher shim. The profile has not yet been loaded. To improve response
379 // times, start animating an empty window which will be populated via
380 // OnShimLaunch(). Note that if --silent-launch is not also passed, the window
381 // will instead populate via StartupBrowserCreator::Launch(). Shim-initiated
382 // launches will always have --silent-launch.
383 if (base::CommandLine::ForCurrentProcess()->
384 HasSwitch(switches::kShowAppList)) {
385 // Do not show the launcher window when the profile is locked, or if it
386 // can't be displayed unpopulated. In the latter case, the Show will occur
387 // in OnShimLaunch() or AppListService::HandleLaunchCommandLine().
388 const ProfileInfoCache& profile_info_cache =
389 g_browser_process->profile_manager()->GetProfileInfoCache();
390 size_t profile_index = profile_info_cache.
391 GetIndexOfProfileWithPath(profile_path);
392 if (profile_index != std::string::npos &&
393 !profile_info_cache.ProfileIsSigninRequiredAtIndex(profile_index) &&
395 ShowWindowNearDock();
399 void AppListServiceMac::DismissAppList() {
400 if (!IsAppListVisible())
403 NSWindow* app_list_window = GetNativeWindow();
404 // If the app list is currently the main window, it will activate the next
405 // Chrome window when dismissed. But if a different application was active
406 // when the app list was shown, activate that instead.
407 base::scoped_nsobject<NSRunningApplication> prior_app;
408 if ([app_list_window isMainWindow])
409 prior_app.swap(previously_active_application_);
411 previously_active_application_.reset();
413 // If activation is successful, the app list will lose main status and try to
414 // close itself again. It can't be closed in this runloop iteration without
415 // OSX deciding to raise the next Chrome window, and _then_ activating the
416 // application on top. This also occurs if no activation option is given.
417 if ([prior_app activateWithOptions:NSApplicationActivateIgnoringOtherApps])
420 [animation_controller_ animateWindow:app_list_window
421 targetOrigin:last_start_origin_
425 void AppListServiceMac::ShowForCustomLauncherPage(Profile* profile) {
429 void AppListServiceMac::HideCustomLauncherPage() {
433 bool AppListServiceMac::IsAppListVisible() const {
434 return [GetNativeWindow() isVisible] &&
435 ![animation_controller_ isClosing];
438 void AppListServiceMac::EnableAppList(Profile* initial_profile,
439 AppListEnableSource enable_source) {
440 AppListServiceImpl::EnableAppList(initial_profile, enable_source);
441 AppController* controller = [NSApp delegate];
442 [controller initAppShimMenuController];
445 void AppListServiceMac::CreateShortcut() {
446 CreateAppListShim(GetProfilePath(
447 g_browser_process->profile_manager()->user_data_dir()));
450 NSWindow* AppListServiceMac::GetAppListWindow() {
451 return GetNativeWindow();
454 void AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host,
455 apps::AppShimLaunchType launch_type,
456 const std::vector<base::FilePath>& files) {
457 if (GetCurrentAppListProfile() && IsAppListVisible()) {
460 // Start by showing a possibly empty window to handle the case where Chrome
461 // is running, but hasn't yet loaded the app launcher profile.
463 ShowWindowNearDock();
467 // Always close the shim process immediately.
468 host->OnAppLaunchComplete(apps::APP_SHIM_LAUNCH_DUPLICATE_HOST);
471 void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {}
473 void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host,
474 apps::AppShimFocusType focus_type,
475 const std::vector<base::FilePath>& files) {}
477 void AppListServiceMac::OnShimSetHidden(apps::AppShimHandler::Host* host,
480 void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {}
482 void AppListServiceMac::ShowWindowNearDock() {
483 if (IsAppListVisible())
486 NSWindow* window = GetAppListWindow();
488 NSPoint target_origin;
489 GetAppListWindowOrigins(window, &target_origin, &last_start_origin_);
490 [window setFrameOrigin:last_start_origin_];
492 // Before activating, see if an application other than Chrome is currently the
493 // active application, so that it can be reactivated when dismissing.
494 previously_active_application_.reset([ActiveApplicationNotChrome() retain]);
496 [animation_controller_ animateWindow:window
497 targetOrigin:target_origin
499 [window makeKeyAndOrderFront:nil];
500 [NSApp activateIgnoringOtherApps:YES];
501 RecordAppListLaunch();
504 void AppListServiceMac::WindowAnimationDidEnd() {
505 [animation_controller_ cleanupOnUIThread];
509 AppListService* AppListService::Get(chrome::HostDesktopType desktop_type) {
510 return GetActiveInstance();
514 void AppListService::InitAll(Profile* initial_profile,
515 const base::FilePath& profile_path) {
516 GetActiveInstance()->InitWithProfilePath(initial_profile, profile_path);
519 @implementation AppListAnimationController
525 - (void)animateWindow:(NSWindow*)window
526 targetOrigin:(NSPoint)targetOrigin
527 closing:(BOOL)closing {
528 // First, stop the existing animation, if there is one.
529 [animation_ stopAnimation];
531 NSRect targetFrame = [window frame];
532 targetFrame.origin = targetOrigin;
534 // NSViewAnimation has a quirk when setting the curve to NSAnimationEaseOut
535 // where it attempts to auto-reverse the animation. FadeOut becomes FadeIn
536 // (good), but FrameKey is also switched (bad). So |targetFrame| needs to be
537 // put on the StartFrameKey when using NSAnimationEaseOut for showing.
538 NSArray* animationArray = @[
540 NSViewAnimationTargetKey : window,
541 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect,
542 (closing ? NSViewAnimationEndFrameKey : NSViewAnimationStartFrameKey) :
543 [NSValue valueWithRect:targetFrame]
547 [[NSViewAnimation alloc] initWithViewAnimations:animationArray]);
548 [animation_ setDuration:kAnimationDuration];
549 [animation_ setDelegate:self];
552 [animation_ setAnimationCurve:NSAnimationEaseIn];
553 window_.reset([window retain]);
555 [window setAlphaValue:0.0f];
556 [animation_ setAnimationCurve:NSAnimationEaseOut];
559 // This once used a threaded animation, but AppKit would too often ignore
560 // -[NSView canDrawConcurrently:] and just redraw whole view hierarchies on
561 // the animation thread anyway, creating a minefield of race conditions.
562 // Non-threaded means the animation isn't as smooth and doesn't begin unless
563 // the UI runloop has spun up (after profile loading).
564 [animation_ setAnimationBlockingMode:NSAnimationNonblocking];
566 [animation_ startAnimation];
569 - (void)cleanupOnUIThread {
570 bool closing = [self isClosing];
576 apps::AppShimHandler::MaybeTerminate();
579 - (void)animationDidEnd:(NSAnimation*)animation {
580 content::BrowserThread::PostTask(
581 content::BrowserThread::UI,
583 base::Bind(&AppListServiceMac::WindowAnimationDidEnd,
584 base::Unretained(GetActiveInstance())));