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