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