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