app_list_service_mac.mm revision 868fa2fe829687343ffae624259930155e16dbd8
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 // TODO(tapted): Add more icon scales when the resource bundle will use them 132 // properly. See http://crbug.com/167408 and http://crbug.com/241304 . 133 shortcut_info.favicon.Add( 134 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128)); 135 136 // TODO(tapted): Create a dock icon using chrome/browser/mac/dock.h . 137 web_app::CreateShortcuts(shortcut_info, 138 ShellIntegration::ShortcutLocations()); 139} 140 141// Check that there is an app list shim. If enabling and there is not, make one. 142// If disabling with --enable-app-list-shim=0, and there is one, delete it. 143void CheckAppListShimOnFileThread(const base::FilePath& profile_path) { 144 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 145 const bool enable = 146 CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppListShim); 147 base::FilePath install_path = web_app::GetAppInstallPath( 148 GetAppListShortcutInfo(profile_path)); 149 if (enable == file_util::PathExists(install_path)) 150 return; 151 152 if (enable) { 153 content::BrowserThread::PostTask( 154 content::BrowserThread::UI, FROM_HERE, 155 base::Bind(&CreateAppListShim, profile_path)); 156 return; 157 } 158 159 // Sanity check because deleting things recursively is scary. 160 CHECK(install_path.MatchesExtension(".app")); 161 file_util::Delete(install_path, true /* recursive */); 162} 163 164AppListControllerDelegateCocoa::AppListControllerDelegateCocoa() {} 165 166AppListControllerDelegateCocoa::~AppListControllerDelegateCocoa() {} 167 168void AppListControllerDelegateCocoa::DismissView() { 169 AppListServiceMac::GetInstance()->DismissAppList(); 170} 171 172gfx::NativeWindow AppListControllerDelegateCocoa::GetAppListWindow() { 173 return AppListServiceMac::GetInstance()->GetAppListWindow(); 174} 175 176bool AppListControllerDelegateCocoa::CanPin() { 177 return false; 178} 179 180bool AppListControllerDelegateCocoa::CanShowCreateShortcutsDialog() { 181 // TODO(tapted): Return true when create shortcuts menu is tested on mac. 182 return false; 183} 184 185void AppListControllerDelegateCocoa::ActivateApp( 186 Profile* profile, const extensions::Extension* extension, int event_flags) { 187 LaunchApp(profile, extension, event_flags); 188} 189 190void AppListControllerDelegateCocoa::LaunchApp( 191 Profile* profile, const extensions::Extension* extension, int event_flags) { 192 chrome::OpenApplication(chrome::AppLaunchParams( 193 profile, extension, NEW_FOREGROUND_TAB)); 194} 195 196void AppListServiceMac::CreateAppList(Profile* requested_profile) { 197 if (profile() == requested_profile) 198 return; 199 200 // The Objective C objects might be released at some unknown point in the 201 // future, so explicitly clear references to C++ objects. 202 [[window_controller_ appListViewController] 203 setDelegate:scoped_ptr<app_list::AppListViewDelegate>(NULL)]; 204 205 SetProfile(requested_profile); 206 scoped_ptr<app_list::AppListViewDelegate> delegate( 207 new AppListViewDelegate(new AppListControllerDelegateCocoa(), profile())); 208 window_controller_.reset([[AppListWindowController alloc] init]); 209 [[window_controller_ appListViewController] setDelegate:delegate.Pass()]; 210} 211 212void AppListServiceMac::Init(Profile* initial_profile) { 213 // On Mac, Init() is called multiple times for a process: any time there is no 214 // browser window open and a new window is opened, and during process startup 215 // to handle the silent launch case (e.g. for app shims). In the startup case, 216 // a profile has not yet been determined so |initial_profile| will be NULL. 217 if (initial_profile) { 218 static bool checked_shim = false; 219 if (!checked_shim) { 220 checked_shim = true; 221 content::BrowserThread::PostTask( 222 content::BrowserThread::FILE, FROM_HERE, 223 base::Bind(&CheckAppListShimOnFileThread, 224 initial_profile->GetPath())); 225 } 226 } 227 228 static bool init_called = false; 229 if (init_called) 230 return; 231 232 init_called = true; 233 apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId, 234 AppListServiceMac::GetInstance()); 235} 236 237void AppListServiceMac::ShowAppList(Profile* requested_profile) { 238 InvalidatePendingProfileLoads(); 239 240 if (IsAppListVisible() && (requested_profile == profile())) { 241 ShowWindowNearDock(); 242 return; 243 } 244 245 SaveProfilePathToLocalState(requested_profile->GetPath()); 246 247 DismissAppList(); 248 CreateAppList(requested_profile); 249 ShowWindowNearDock(); 250} 251 252void AppListServiceMac::DismissAppList() { 253 if (!IsAppListVisible()) 254 return; 255 256 [[window_controller_ window] close]; 257 258 FOR_EACH_OBSERVER(apps::AppShimHandler::Host, 259 observers_, 260 OnAppClosed()); 261} 262 263bool AppListServiceMac::IsAppListVisible() const { 264 return [[window_controller_ window] isVisible]; 265} 266 267void AppListServiceMac::EnableAppList() { 268 // TODO(tapted): Implement enable logic here for OSX. 269} 270 271NSWindow* AppListServiceMac::GetAppListWindow() { 272 return [window_controller_ window]; 273} 274 275bool AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host, 276 apps::AppShimLaunchType launch_type) { 277 ShowForSavedProfile(); 278 observers_.AddObserver(host); 279 return true; 280} 281 282void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) { 283 observers_.RemoveObserver(host); 284 DismissAppList(); 285} 286 287void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host) { 288 DismissAppList(); 289} 290 291void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) { 292 DismissAppList(); 293} 294 295enum DockLocation { 296 DockLocationOtherDisplay, 297 DockLocationBottom, 298 DockLocationLeft, 299 DockLocationRight, 300}; 301 302DockLocation DockLocationInDisplay(const gfx::Display& display) { 303 // Assume the dock occupies part of the work area either on the left, right or 304 // bottom of the display. Note in the autohide case, it is always 4 pixels. 305 const gfx::Rect work_area = display.work_area(); 306 const gfx::Rect display_bounds = display.bounds(); 307 if (work_area.bottom() != display_bounds.bottom()) 308 return DockLocationBottom; 309 310 if (work_area.x() != display_bounds.x()) 311 return DockLocationLeft; 312 313 if (work_area.right() != display_bounds.right()) 314 return DockLocationRight; 315 316 return DockLocationOtherDisplay; 317} 318 319NSPoint GetAppListWindowOrigin(NSWindow* window) { 320 gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]); 321 gfx::Point anchor = screen->GetCursorScreenPoint(); 322 const gfx::Display display = screen->GetDisplayNearestPoint(anchor); 323 const DockLocation dock_location = DockLocationInDisplay(display); 324 const gfx::Rect display_bounds = display.bounds(); 325 326 // Ensure y coordinates are flipped back into AppKit's coordinate system. 327 const CGFloat max_y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]); 328 if (dock_location == DockLocationOtherDisplay) { 329 // Just display at the bottom-left of the display the cursor is on. 330 return NSMakePoint(display_bounds.x(), 331 max_y - display_bounds.bottom()); 332 } 333 334 // Anchor the center of the window in a region that prevents the window 335 // showing outside of the work area. 336 const NSSize window_size = [window frame].size; 337 gfx::Rect anchor_area = display.work_area(); 338 anchor_area.Inset(window_size.width / 2, window_size.height / 2); 339 anchor.SetToMax(anchor_area.origin()); 340 anchor.SetToMin(anchor_area.bottom_right()); 341 342 // Move anchor to the dock, keeping the other axis aligned with the cursor. 343 switch (dock_location) { 344 case DockLocationBottom: 345 anchor.set_y(anchor_area.bottom()); 346 break; 347 case DockLocationLeft: 348 anchor.set_x(anchor_area.x()); 349 break; 350 case DockLocationRight: 351 anchor.set_x(anchor_area.right()); 352 break; 353 default: 354 NOTREACHED(); 355 } 356 357 return NSMakePoint( 358 anchor.x() - window_size.width / 2, 359 max_y - anchor.y() - window_size.height / 2); 360} 361 362void AppListServiceMac::ShowWindowNearDock() { 363 NSWindow* window = GetAppListWindow(); 364 DCHECK(window); 365 [window setFrameOrigin:GetAppListWindowOrigin(window)]; 366 [window makeKeyAndOrderFront:nil]; 367 [NSApp activateIgnoringOtherApps:YES]; 368} 369 370} // namespace 371 372// static 373AppListService* AppListService::Get() { 374 return AppListServiceMac::GetInstance(); 375} 376 377// static 378void AppListService::InitAll(Profile* initial_profile) { 379 Get()->Init(initial_profile); 380} 381