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