app_list_service_mac.mm revision ca12bfac764ba476d6cd062bf1dde12cc64c3f40
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 "base/bind.h" 11#include "base/command_line.h" 12#include "base/file_util.h" 13#include "base/lazy_instance.h" 14#include "base/mac/scoped_nsobject.h" 15#include "base/memory/singleton.h" 16#include "base/message_loop/message_loop.h" 17#include "base/observer_list.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.h" 23#include "chrome/browser/ui/app_list/app_list_service.h" 24#include "chrome/browser/ui/app_list/app_list_service_impl.h" 25#include "chrome/browser/ui/app_list/app_list_view_delegate.h" 26#include "chrome/browser/ui/extensions/application_launch.h" 27#include "chrome/browser/ui/web_applications/web_app_ui.h" 28#include "chrome/browser/web_applications/web_app.h" 29#include "chrome/browser/web_applications/web_app_mac.h" 30#include "chrome/common/chrome_switches.h" 31#include "chrome/common/chrome_version_info.h" 32#include "chrome/common/mac/app_mode_common.h" 33#include "content/public/browser/browser_thread.h" 34#include "grit/chrome_unscaled_resources.h" 35#include "grit/google_chrome_strings.h" 36#import "ui/app_list/cocoa/app_list_view_controller.h" 37#import "ui/app_list/cocoa/app_list_window_controller.h" 38#include "ui/base/l10n/l10n_util.h" 39#include "ui/base/resource/resource_bundle.h" 40#include "ui/gfx/display.h" 41#include "ui/gfx/screen.h" 42 43namespace gfx { 44class ImageSkia; 45} 46 47namespace { 48 49// AppListServiceMac manages global resources needed for the app list to 50// operate, and controls when the app list is opened and closed. 51class AppListServiceMac : public AppListServiceImpl, 52 public apps::AppShimHandler { 53 public: 54 virtual ~AppListServiceMac() {} 55 56 static AppListServiceMac* GetInstance() { 57 return Singleton<AppListServiceMac, 58 LeakySingletonTraits<AppListServiceMac> >::get(); 59 } 60 61 void CreateAppList(Profile* profile); 62 void ShowWindowNearDock(); 63 64 // AppListService overrides: 65 virtual void Init(Profile* initial_profile) OVERRIDE; 66 virtual void ShowForProfile(Profile* requested_profile) OVERRIDE; 67 virtual void DismissAppList() OVERRIDE; 68 virtual bool IsAppListVisible() const OVERRIDE; 69 virtual gfx::NativeWindow GetAppListWindow() OVERRIDE; 70 71 // AppListServiceImpl overrides: 72 virtual void CreateShortcut() OVERRIDE; 73 virtual void OnSigninStatusChanged() OVERRIDE; 74 75 // AppShimHandler overrides: 76 virtual void OnShimLaunch(apps::AppShimHandler::Host* host, 77 apps::AppShimLaunchType launch_type) OVERRIDE; 78 virtual void OnShimClose(apps::AppShimHandler::Host* host) OVERRIDE; 79 virtual void OnShimFocus(apps::AppShimHandler::Host* host, 80 apps::AppShimFocusType focus_type) OVERRIDE; 81 virtual void OnShimSetHidden(apps::AppShimHandler::Host* host, 82 bool hidden) OVERRIDE; 83 virtual void OnShimQuit(apps::AppShimHandler::Host* host) OVERRIDE; 84 85 private: 86 friend struct DefaultSingletonTraits<AppListServiceMac>; 87 88 AppListServiceMac() {} 89 90 base::scoped_nsobject<AppListWindowController> window_controller_; 91 base::scoped_nsobject<NSRunningApplication> previously_active_application_; 92 93 // App shim hosts observing when the app list is dismissed. In normal user 94 // usage there should only be one. However, it can't be guaranteed, so use 95 // an ObserverList rather than handling corner cases. 96 ObserverList<apps::AppShimHandler::Host> observers_; 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 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 // TODO(tapted): Create a dock icon using chrome/browser/mac/dock.h . 175 web_app::CreateShortcuts(shortcut_info, 176 ShellIntegration::ShortcutLocations(), 177 web_app::ALLOW_DUPLICATE_SHORTCUTS); 178} 179 180// Check that there is an app list shim. If enabling and there is not, make one. 181// If the flag is not present, and there is a shim, delete it. 182void CheckAppListShimOnFileThread(const base::FilePath& profile_path) { 183 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 184 const bool enable = 185 CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppListShim); 186 base::FilePath install_path = web_app::GetAppInstallPath( 187 GetAppListShortcutInfo(profile_path)); 188 if (enable == base::PathExists(install_path)) 189 return; 190 191 if (enable) { 192 content::BrowserThread::PostTask( 193 content::BrowserThread::UI, FROM_HERE, 194 base::Bind(&CreateAppListShim, profile_path)); 195 return; 196 } 197 198 // Sanity check because deleting things recursively is scary. 199 CHECK(install_path.MatchesExtension(".app")); 200 base::DeleteFile(install_path, true /* recursive */); 201} 202 203void CreateShortcutsInDefaultLocation( 204 const ShellIntegration::ShortcutInfo& shortcut_info) { 205 web_app::CreateShortcuts(shortcut_info, 206 ShellIntegration::ShortcutLocations(), 207 web_app::ALLOW_DUPLICATE_SHORTCUTS); 208} 209 210NSRunningApplication* ActiveApplicationNotChrome() { 211 NSArray* applications = [[NSWorkspace sharedWorkspace] runningApplications]; 212 for (NSRunningApplication* application in applications) { 213 if (![application isActive]) 214 continue; 215 216 if ([application isEqual:[NSRunningApplication currentApplication]]) 217 return nil; // Chrome is active. 218 219 return application; 220 } 221 222 return nil; 223} 224 225AppListControllerDelegateCocoa::AppListControllerDelegateCocoa() {} 226 227AppListControllerDelegateCocoa::~AppListControllerDelegateCocoa() {} 228 229void AppListControllerDelegateCocoa::DismissView() { 230 AppListServiceMac::GetInstance()->DismissAppList(); 231} 232 233gfx::NativeWindow AppListControllerDelegateCocoa::GetAppListWindow() { 234 return AppListServiceMac::GetInstance()->GetAppListWindow(); 235} 236 237bool AppListControllerDelegateCocoa::CanPin() { 238 return false; 239} 240 241bool AppListControllerDelegateCocoa::CanDoCreateShortcutsFlow( 242 bool is_platform_app) { 243 return is_platform_app && 244 CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppShims); 245} 246 247void AppListControllerDelegateCocoa::DoCreateShortcutsFlow( 248 Profile* profile, const std::string& extension_id) { 249 ExtensionService* service = 250 extensions::ExtensionSystem::Get(profile)->extension_service(); 251 DCHECK(service); 252 const extensions::Extension* extension = 253 service->GetInstalledExtension(extension_id); 254 DCHECK(extension); 255 256 web_app::UpdateShortcutInfoAndIconForApp( 257 *extension, profile, base::Bind(&CreateShortcutsInDefaultLocation)); 258} 259 260void AppListControllerDelegateCocoa::ActivateApp( 261 Profile* profile, const extensions::Extension* extension, int event_flags) { 262 LaunchApp(profile, extension, event_flags); 263} 264 265void AppListControllerDelegateCocoa::LaunchApp( 266 Profile* profile, const extensions::Extension* extension, int event_flags) { 267 chrome::OpenApplication(chrome::AppLaunchParams( 268 profile, extension, NEW_FOREGROUND_TAB)); 269} 270 271void AppListServiceMac::CreateAppList(Profile* requested_profile) { 272 if (profile() == requested_profile) 273 return; 274 275 // The Objective C objects might be released at some unknown point in the 276 // future, so explicitly clear references to C++ objects. 277 [[window_controller_ appListViewController] 278 setDelegate:scoped_ptr<app_list::AppListViewDelegate>()]; 279 280 SetProfile(requested_profile); 281 scoped_ptr<app_list::AppListViewDelegate> delegate( 282 new AppListViewDelegate(new AppListControllerDelegateCocoa(), profile())); 283 window_controller_.reset([[AppListWindowController alloc] init]); 284 [[window_controller_ appListViewController] setDelegate:delegate.Pass()]; 285} 286 287void AppListServiceMac::Init(Profile* initial_profile) { 288 // On Mac, Init() is called multiple times for a process: any time there is no 289 // browser window open and a new window is opened, and during process startup 290 // to handle the silent launch case (e.g. for app shims). In the startup case, 291 // a profile has not yet been determined so |initial_profile| will be NULL. 292 static bool init_called_with_profile = false; 293 if (initial_profile && !init_called_with_profile) { 294 init_called_with_profile = true; 295 HandleCommandLineFlags(initial_profile); 296 if (!apps::IsAppLauncherEnabled()) { 297 // Not yet enabled via the Web Store. Check for the chrome://flag. 298 content::BrowserThread::PostTask( 299 content::BrowserThread::FILE, FROM_HERE, 300 base::Bind(&CheckAppListShimOnFileThread, 301 initial_profile->GetPath())); 302 } 303 } 304 305 static bool init_called = false; 306 if (init_called) 307 return; 308 309 init_called = true; 310 apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId, 311 AppListServiceMac::GetInstance()); 312} 313 314void AppListServiceMac::ShowForProfile(Profile* requested_profile) { 315 InvalidatePendingProfileLoads(); 316 317 if (IsAppListVisible() && (requested_profile == profile())) { 318 ShowWindowNearDock(); 319 return; 320 } 321 322 SetProfilePath(requested_profile->GetPath()); 323 324 DismissAppList(); 325 CreateAppList(requested_profile); 326 ShowWindowNearDock(); 327} 328 329void AppListServiceMac::DismissAppList() { 330 if (!IsAppListVisible()) 331 return; 332 333 // If the app list is currently the main window, it will activate the next 334 // Chrome window when dismissed. But if a different application was active 335 // when the app list was shown, activate that instead. 336 base::scoped_nsobject<NSRunningApplication> prior_app; 337 if ([[window_controller_ window] isMainWindow]) 338 prior_app.swap(previously_active_application_); 339 else 340 previously_active_application_.reset(); 341 342 // If activation is successful, the app list will lose main status and try to 343 // close itself again. It can't be closed in this runloop iteration without 344 // OSX deciding to raise the next Chrome window, and _then_ activating the 345 // application on top. This also occurs if no activation option is given. 346 if ([prior_app activateWithOptions:NSApplicationActivateIgnoringOtherApps]) 347 return; 348 349 [[window_controller_ window] close]; 350 351 FOR_EACH_OBSERVER(apps::AppShimHandler::Host, 352 observers_, 353 OnAppClosed()); 354} 355 356bool AppListServiceMac::IsAppListVisible() const { 357 return [[window_controller_ window] isVisible]; 358} 359 360void AppListServiceMac::CreateShortcut() { 361 CreateAppListShim(GetProfilePath( 362 g_browser_process->profile_manager()->user_data_dir())); 363} 364 365NSWindow* AppListServiceMac::GetAppListWindow() { 366 return [window_controller_ window]; 367} 368 369void AppListServiceMac::OnSigninStatusChanged() { 370 [[window_controller_ appListViewController] onSigninStatusChanged]; 371} 372 373void AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host, 374 apps::AppShimLaunchType launch_type) { 375 Show(); 376 observers_.AddObserver(host); 377 host->OnAppLaunchComplete(apps::APP_SHIM_LAUNCH_SUCCESS); 378} 379 380void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) { 381 observers_.RemoveObserver(host); 382 DismissAppList(); 383} 384 385void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host, 386 apps::AppShimFocusType focus_type) { 387 DismissAppList(); 388} 389 390void AppListServiceMac::OnShimSetHidden(apps::AppShimHandler::Host* host, 391 bool hidden) {} 392 393void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) { 394 DismissAppList(); 395} 396 397enum DockLocation { 398 DockLocationOtherDisplay, 399 DockLocationBottom, 400 DockLocationLeft, 401 DockLocationRight, 402}; 403 404DockLocation DockLocationInDisplay(const gfx::Display& display) { 405 // Assume the dock occupies part of the work area either on the left, right or 406 // bottom of the display. Note in the autohide case, it is always 4 pixels. 407 const gfx::Rect work_area = display.work_area(); 408 const gfx::Rect display_bounds = display.bounds(); 409 if (work_area.bottom() != display_bounds.bottom()) 410 return DockLocationBottom; 411 412 if (work_area.x() != display_bounds.x()) 413 return DockLocationLeft; 414 415 if (work_area.right() != display_bounds.right()) 416 return DockLocationRight; 417 418 return DockLocationOtherDisplay; 419} 420 421// If |work_area_edge| is too close to the |screen_edge| (e.g. autohide dock), 422// adjust |anchor| away from the edge by a constant amount to reduce overlap and 423// ensure the dock icon can still be clicked to dismiss the app list. 424int AdjustPointForDynamicDock(int anchor, int screen_edge, int work_area_edge) { 425 const int kAutohideDockThreshold = 10; 426 const int kExtraDistance = 50; // A dock with 40 items is about this size. 427 if (abs(work_area_edge - screen_edge) > kAutohideDockThreshold) 428 return anchor; 429 430 return anchor + 431 (screen_edge < work_area_edge ? kExtraDistance : -kExtraDistance); 432} 433 434NSPoint GetAppListWindowOrigin(NSWindow* window) { 435 gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]); 436 // Ensure y coordinates are flipped back into AppKit's coordinate system. 437 const CGFloat max_y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]); 438 if (!CGCursorIsVisible()) { 439 // If Chrome is the active application, display on the same display as 440 // Chrome's keyWindow since this will catch activations triggered, e.g, via 441 // WebStore install. If another application is active, OSX doesn't provide a 442 // reliable way to get the display in use. Fall back to the primary display 443 // since it has the menu bar and is likely to be correct, e.g., for 444 // activations from Spotlight. 445 const gfx::NativeView key_view = [[NSApp keyWindow] contentView]; 446 const gfx::Rect work_area = key_view && [NSApp isActive] ? 447 screen->GetDisplayNearestWindow(key_view).work_area() : 448 screen->GetPrimaryDisplay().work_area(); 449 return NSMakePoint(work_area.x(), max_y - work_area.bottom()); 450 } 451 452 gfx::Point anchor = screen->GetCursorScreenPoint(); 453 const gfx::Display display = screen->GetDisplayNearestPoint(anchor); 454 const DockLocation dock_location = DockLocationInDisplay(display); 455 const gfx::Rect display_bounds = display.bounds(); 456 457 if (dock_location == DockLocationOtherDisplay) { 458 // Just display at the bottom-left of the display the cursor is on. 459 return NSMakePoint(display_bounds.x(), max_y - display_bounds.bottom()); 460 } 461 462 // Anchor the center of the window in a region that prevents the window 463 // showing outside of the work area. 464 const NSSize window_size = [window frame].size; 465 const gfx::Rect work_area = display.work_area(); 466 gfx::Rect anchor_area = work_area; 467 anchor_area.Inset(window_size.width / 2, window_size.height / 2); 468 anchor.SetToMax(anchor_area.origin()); 469 anchor.SetToMin(anchor_area.bottom_right()); 470 471 // Move anchor to the dock, keeping the other axis aligned with the cursor. 472 switch (dock_location) { 473 case DockLocationBottom: 474 anchor.set_y(AdjustPointForDynamicDock( 475 anchor_area.bottom(), display_bounds.bottom(), work_area.bottom())); 476 break; 477 case DockLocationLeft: 478 anchor.set_x(AdjustPointForDynamicDock( 479 anchor_area.x(), display_bounds.x(), work_area.x())); 480 break; 481 case DockLocationRight: 482 anchor.set_x(AdjustPointForDynamicDock( 483 anchor_area.right(), display_bounds.right(), work_area.right())); 484 break; 485 default: 486 NOTREACHED(); 487 } 488 489 return NSMakePoint( 490 anchor.x() - window_size.width / 2, 491 max_y - anchor.y() - window_size.height / 2); 492} 493 494void AppListServiceMac::ShowWindowNearDock() { 495 NSWindow* window = GetAppListWindow(); 496 DCHECK(window); 497 [window setFrameOrigin:GetAppListWindowOrigin(window)]; 498 499 // Before activating, see if an application other than Chrome is currently the 500 // active application, so that it can be reactivated when dismissing. 501 previously_active_application_.reset([ActiveApplicationNotChrome() retain]); 502 503 [window makeKeyAndOrderFront:nil]; 504 [NSApp activateIgnoringOtherApps:YES]; 505} 506 507} // namespace 508 509// static 510AppListService* AppListService::Get() { 511 return AppListServiceMac::GetInstance(); 512} 513 514// static 515void AppListService::InitAll(Profile* initial_profile) { 516 Get()->Init(initial_profile); 517} 518