app_list_service_mac.mm revision 7d4cd473f85ac64c3747c96c277f9e506a0d2246
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 "apps/app_shim/app_shim_handler_mac.h" 6#include "base/bind.h" 7#include "base/command_line.h" 8#include "base/file_util.h" 9#include "base/lazy_instance.h" 10#include "base/memory/scoped_nsobject.h" 11#include "base/memory/singleton.h" 12#include "base/message_loop.h" 13#include "base/observer_list.h" 14#include "chrome/browser/ui/app_list/app_list_controller_delegate.h" 15#include "chrome/browser/ui/app_list/app_list_service.h" 16#include "chrome/browser/ui/app_list/app_list_service_impl.h" 17#include "chrome/browser/ui/app_list/app_list_view_delegate.h" 18#include "chrome/browser/ui/extensions/application_launch.h" 19#include "chrome/browser/web_applications/web_app.h" 20#include "chrome/browser/web_applications/web_app_mac.h" 21#include "chrome/common/chrome_switches.h" 22#include "chrome/common/chrome_version_info.h" 23#include "chrome/common/mac/app_mode_common.h" 24#include "content/public/browser/browser_thread.h" 25#include "grit/chrome_unscaled_resources.h" 26#include "grit/google_chrome_strings.h" 27#import "ui/app_list/cocoa/app_list_view_controller.h" 28#import "ui/app_list/cocoa/app_list_window_controller.h" 29#include "ui/base/l10n/l10n_util.h" 30#include "ui/base/resource/resource_bundle.h" 31#include "ui/gfx/display.h" 32#include "ui/gfx/screen.h" 33 34namespace gfx { 35class ImageSkia; 36} 37 38namespace { 39 40// AppListServiceMac manages global resources needed for the app list to 41// operate, and controls when the app list is opened and closed. 42class AppListServiceMac : public AppListServiceImpl, 43 public apps::AppShimHandler { 44 public: 45 virtual ~AppListServiceMac() {} 46 47 static AppListServiceMac* GetInstance() { 48 return Singleton<AppListServiceMac, 49 LeakySingletonTraits<AppListServiceMac> >::get(); 50 } 51 52 void CreateAppList(Profile* profile); 53 void ShowWindowNearDock(); 54 55 // AppListService overrides: 56 virtual void Init(Profile* initial_profile) OVERRIDE; 57 virtual void ShowAppList(Profile* requested_profile) OVERRIDE; 58 virtual void DismissAppList() OVERRIDE; 59 virtual bool IsAppListVisible() const OVERRIDE; 60 virtual void EnableAppList() OVERRIDE; 61 virtual gfx::NativeWindow GetAppListWindow() OVERRIDE; 62 63 // AppShimHandler overrides: 64 virtual bool OnShimLaunch(apps::AppShimHandler::Host* host, 65 apps::AppShimLaunchType launch_type) OVERRIDE; 66 virtual void OnShimClose(apps::AppShimHandler::Host* host) OVERRIDE; 67 virtual void OnShimFocus(apps::AppShimHandler::Host* host) OVERRIDE; 68 virtual void OnShimQuit(apps::AppShimHandler::Host* host) OVERRIDE; 69 70 private: 71 friend struct DefaultSingletonTraits<AppListServiceMac>; 72 73 AppListServiceMac() {} 74 75 scoped_nsobject<AppListWindowController> window_controller_; 76 77 // App shim hosts observing when the app list is dismissed. In normal user 78 // usage there should only be one. However, it can't be guaranteed, so use 79 // an ObserverList rather than handling corner cases. 80 ObserverList<apps::AppShimHandler::Host> observers_; 81 82 DISALLOW_COPY_AND_ASSIGN(AppListServiceMac); 83}; 84 85class AppListControllerDelegateCocoa : public AppListControllerDelegate { 86 public: 87 AppListControllerDelegateCocoa(); 88 virtual ~AppListControllerDelegateCocoa(); 89 90 private: 91 // AppListControllerDelegate overrides: 92 virtual void DismissView() OVERRIDE; 93 virtual gfx::NativeWindow GetAppListWindow() OVERRIDE; 94 virtual bool CanPin() OVERRIDE; 95 virtual bool CanShowCreateShortcutsDialog() OVERRIDE; 96 virtual void ActivateApp(Profile* profile, 97 const extensions::Extension* extension, 98 int event_flags) OVERRIDE; 99 virtual void LaunchApp(Profile* profile, 100 const extensions::Extension* extension, 101 int event_flags) OVERRIDE; 102 103 DISALLOW_COPY_AND_ASSIGN(AppListControllerDelegateCocoa); 104}; 105 106ShellIntegration::ShortcutInfo GetAppListShortcutInfo( 107 const base::FilePath& profile_path) { 108 ShellIntegration::ShortcutInfo shortcut_info; 109 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); 110 if (channel == chrome::VersionInfo::CHANNEL_CANARY) { 111 shortcut_info.title = 112 l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME_CANARY); 113 } else { 114 shortcut_info.title = l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME); 115 } 116 117 shortcut_info.extension_id = app_mode::kAppListModeId; 118 shortcut_info.description = shortcut_info.title; 119 shortcut_info.profile_path = profile_path; 120 121 return shortcut_info; 122} 123 124void CreateAppListShim(const base::FilePath& profile_path) { 125 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 126 WebApplicationInfo web_app_info; 127 ShellIntegration::ShortcutInfo shortcut_info = 128 GetAppListShortcutInfo(profile_path); 129 130 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance(); 131 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); 132 if (channel == chrome::VersionInfo::CHANNEL_CANARY) { 133#if defined(GOOGLE_CHROME_BUILD) 134 shortcut_info.favicon.Add( 135 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_16)); 136 shortcut_info.favicon.Add( 137 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_32)); 138 shortcut_info.favicon.Add( 139 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_128)); 140 shortcut_info.favicon.Add( 141 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_256)); 142#else 143 NOTREACHED(); 144#endif 145 } else { 146 shortcut_info.favicon.Add( 147 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16)); 148 shortcut_info.favicon.Add( 149 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32)); 150 shortcut_info.favicon.Add( 151 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128)); 152 shortcut_info.favicon.Add( 153 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256)); 154 } 155 156 // TODO(tapted): Create a dock icon using chrome/browser/mac/dock.h . 157 web_app::CreateShortcuts(shortcut_info, 158 ShellIntegration::ShortcutLocations()); 159} 160 161// Check that there is an app list shim. If enabling and there is not, make one. 162// If disabling with --enable-app-list-shim=0, and there is one, delete it. 163void CheckAppListShimOnFileThread(const base::FilePath& profile_path) { 164 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 165 const bool enable = 166 CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppListShim); 167 base::FilePath install_path = web_app::GetAppInstallPath( 168 GetAppListShortcutInfo(profile_path)); 169 if (enable == file_util::PathExists(install_path)) 170 return; 171 172 if (enable) { 173 content::BrowserThread::PostTask( 174 content::BrowserThread::UI, FROM_HERE, 175 base::Bind(&CreateAppListShim, profile_path)); 176 return; 177 } 178 179 // Sanity check because deleting things recursively is scary. 180 CHECK(install_path.MatchesExtension(".app")); 181 file_util::Delete(install_path, true /* recursive */); 182} 183 184AppListControllerDelegateCocoa::AppListControllerDelegateCocoa() {} 185 186AppListControllerDelegateCocoa::~AppListControllerDelegateCocoa() {} 187 188void AppListControllerDelegateCocoa::DismissView() { 189 AppListServiceMac::GetInstance()->DismissAppList(); 190} 191 192gfx::NativeWindow AppListControllerDelegateCocoa::GetAppListWindow() { 193 return AppListServiceMac::GetInstance()->GetAppListWindow(); 194} 195 196bool AppListControllerDelegateCocoa::CanPin() { 197 return false; 198} 199 200bool AppListControllerDelegateCocoa::CanShowCreateShortcutsDialog() { 201 // TODO(tapted): Return true when create shortcuts menu is tested on mac. 202 return false; 203} 204 205void AppListControllerDelegateCocoa::ActivateApp( 206 Profile* profile, const extensions::Extension* extension, int event_flags) { 207 LaunchApp(profile, extension, event_flags); 208} 209 210void AppListControllerDelegateCocoa::LaunchApp( 211 Profile* profile, const extensions::Extension* extension, int event_flags) { 212 chrome::OpenApplication(chrome::AppLaunchParams( 213 profile, extension, NEW_FOREGROUND_TAB)); 214} 215 216void AppListServiceMac::CreateAppList(Profile* requested_profile) { 217 if (profile() == requested_profile) 218 return; 219 220 // The Objective C objects might be released at some unknown point in the 221 // future, so explicitly clear references to C++ objects. 222 [[window_controller_ appListViewController] 223 setDelegate:scoped_ptr<app_list::AppListViewDelegate>(NULL)]; 224 225 SetProfile(requested_profile); 226 scoped_ptr<app_list::AppListViewDelegate> delegate( 227 new AppListViewDelegate(new AppListControllerDelegateCocoa(), profile())); 228 window_controller_.reset([[AppListWindowController alloc] init]); 229 [[window_controller_ appListViewController] setDelegate:delegate.Pass()]; 230} 231 232void AppListServiceMac::Init(Profile* initial_profile) { 233 // On Mac, Init() is called multiple times for a process: any time there is no 234 // browser window open and a new window is opened, and during process startup 235 // to handle the silent launch case (e.g. for app shims). In the startup case, 236 // a profile has not yet been determined so |initial_profile| will be NULL. 237 if (initial_profile) { 238 static bool checked_shim = false; 239 if (!checked_shim) { 240 checked_shim = true; 241 content::BrowserThread::PostTask( 242 content::BrowserThread::FILE, FROM_HERE, 243 base::Bind(&CheckAppListShimOnFileThread, 244 initial_profile->GetPath())); 245 } 246 } 247 248 static bool init_called = false; 249 if (init_called) 250 return; 251 252 init_called = true; 253 apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId, 254 AppListServiceMac::GetInstance()); 255} 256 257void AppListServiceMac::ShowAppList(Profile* requested_profile) { 258 InvalidatePendingProfileLoads(); 259 260 if (IsAppListVisible() && (requested_profile == profile())) { 261 ShowWindowNearDock(); 262 return; 263 } 264 265 SaveProfilePathToLocalState(requested_profile->GetPath()); 266 267 DismissAppList(); 268 CreateAppList(requested_profile); 269 ShowWindowNearDock(); 270} 271 272void AppListServiceMac::DismissAppList() { 273 if (!IsAppListVisible()) 274 return; 275 276 [[window_controller_ window] close]; 277 278 FOR_EACH_OBSERVER(apps::AppShimHandler::Host, 279 observers_, 280 OnAppClosed()); 281} 282 283bool AppListServiceMac::IsAppListVisible() const { 284 return [[window_controller_ window] isVisible]; 285} 286 287void AppListServiceMac::EnableAppList() { 288 // TODO(tapted): Implement enable logic here for OSX. 289} 290 291NSWindow* AppListServiceMac::GetAppListWindow() { 292 return [window_controller_ window]; 293} 294 295bool AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host, 296 apps::AppShimLaunchType launch_type) { 297 ShowForSavedProfile(); 298 observers_.AddObserver(host); 299 return true; 300} 301 302void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) { 303 observers_.RemoveObserver(host); 304 DismissAppList(); 305} 306 307void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host) { 308 DismissAppList(); 309} 310 311void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) { 312 DismissAppList(); 313} 314 315enum DockLocation { 316 DockLocationOtherDisplay, 317 DockLocationBottom, 318 DockLocationLeft, 319 DockLocationRight, 320}; 321 322DockLocation DockLocationInDisplay(const gfx::Display& display) { 323 // Assume the dock occupies part of the work area either on the left, right or 324 // bottom of the display. Note in the autohide case, it is always 4 pixels. 325 const gfx::Rect work_area = display.work_area(); 326 const gfx::Rect display_bounds = display.bounds(); 327 if (work_area.bottom() != display_bounds.bottom()) 328 return DockLocationBottom; 329 330 if (work_area.x() != display_bounds.x()) 331 return DockLocationLeft; 332 333 if (work_area.right() != display_bounds.right()) 334 return DockLocationRight; 335 336 return DockLocationOtherDisplay; 337} 338 339NSPoint GetAppListWindowOrigin(NSWindow* window) { 340 gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]); 341 gfx::Point anchor = screen->GetCursorScreenPoint(); 342 const gfx::Display display = screen->GetDisplayNearestPoint(anchor); 343 const DockLocation dock_location = DockLocationInDisplay(display); 344 const gfx::Rect display_bounds = display.bounds(); 345 346 // Ensure y coordinates are flipped back into AppKit's coordinate system. 347 const CGFloat max_y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]); 348 if (dock_location == DockLocationOtherDisplay) { 349 // Just display at the bottom-left of the display the cursor is on. 350 return NSMakePoint(display_bounds.x(), 351 max_y - display_bounds.bottom()); 352 } 353 354 // Anchor the center of the window in a region that prevents the window 355 // showing outside of the work area. 356 const NSSize window_size = [window frame].size; 357 gfx::Rect anchor_area = display.work_area(); 358 anchor_area.Inset(window_size.width / 2, window_size.height / 2); 359 anchor.SetToMax(anchor_area.origin()); 360 anchor.SetToMin(anchor_area.bottom_right()); 361 362 // Move anchor to the dock, keeping the other axis aligned with the cursor. 363 switch (dock_location) { 364 case DockLocationBottom: 365 anchor.set_y(anchor_area.bottom()); 366 break; 367 case DockLocationLeft: 368 anchor.set_x(anchor_area.x()); 369 break; 370 case DockLocationRight: 371 anchor.set_x(anchor_area.right()); 372 break; 373 default: 374 NOTREACHED(); 375 } 376 377 return NSMakePoint( 378 anchor.x() - window_size.width / 2, 379 max_y - anchor.y() - window_size.height / 2); 380} 381 382void AppListServiceMac::ShowWindowNearDock() { 383 NSWindow* window = GetAppListWindow(); 384 DCHECK(window); 385 [window setFrameOrigin:GetAppListWindowOrigin(window)]; 386 [window makeKeyAndOrderFront:nil]; 387 [NSApp activateIgnoringOtherApps:YES]; 388} 389 390} // namespace 391 392// static 393AppListService* AppListService::Get() { 394 return AppListServiceMac::GetInstance(); 395} 396 397// static 398void AppListService::InitAll(Profile* initial_profile) { 399 Get()->Init(initial_profile); 400} 401