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