extension_app_item.cc revision a93a17c8d99d686bd4a1511e5504e5e6cc9fcadf
1// Copyright (c) 2012 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 "chrome/browser/ui/app_list/extension_app_item.h" 6 7#include "base/prefs/pref_service.h" 8#include "chrome/app/chrome_command_ids.h" 9#include "chrome/browser/extensions/context_menu_matcher.h" 10#include "chrome/browser/extensions/extension_prefs.h" 11#include "chrome/browser/extensions/extension_service.h" 12#include "chrome/browser/extensions/extension_sorting.h" 13#include "chrome/browser/extensions/extension_system.h" 14#include "chrome/browser/extensions/extension_uninstall_dialog.h" 15#include "chrome/browser/extensions/management_policy.h" 16#include "chrome/browser/prefs/incognito_mode_prefs.h" 17#include "chrome/browser/profiles/profile.h" 18#include "chrome/browser/ui/app_list/app_list_controller_delegate.h" 19#include "chrome/browser/ui/browser_navigator.h" 20#include "chrome/browser/ui/browser_tabstrip.h" 21#include "chrome/browser/ui/browser_window.h" 22#include "chrome/browser/ui/extensions/extension_enable_flow.h" 23#include "chrome/browser/ui/webui/ntp/app_launcher_handler.h" 24#include "chrome/common/extensions/extension.h" 25#include "chrome/common/extensions/extension_constants.h" 26#include "chrome/common/extensions/extension_icon_set.h" 27#include "chrome/common/extensions/manifest_handlers/icons_handler.h" 28#include "chrome/common/extensions/manifest_url_handler.h" 29#include "content/public/common/context_menu_params.h" 30#include "grit/chromium_strings.h" 31#include "grit/generated_resources.h" 32#include "grit/theme_resources.h" 33#include "ui/base/l10n/l10n_util.h" 34#include "ui/base/resource/resource_bundle.h" 35#include "ui/gfx/canvas.h" 36#include "ui/gfx/color_utils.h" 37#include "ui/gfx/image/canvas_image_source.h" 38#include "ui/gfx/image/image_skia_operations.h" 39 40#if defined(USE_ASH) 41#include "ash/shell.h" 42#endif 43 44using extensions::Extension; 45 46namespace { 47 48enum CommandId { 49 LAUNCH_NEW = 100, 50 TOGGLE_PIN, 51 CREATE_SHORTCUTS, 52 OPTIONS, 53 UNINSTALL, 54 DETAILS, 55 MENU_NEW_WINDOW, 56 MENU_NEW_INCOGNITO_WINDOW, 57 // Order matters in LAUNCHER_TYPE_xxxx and must match LaunchType. 58 LAUNCH_TYPE_START = 200, 59 LAUNCH_TYPE_PINNED_TAB = LAUNCH_TYPE_START, 60 LAUNCH_TYPE_REGULAR_TAB, 61 LAUNCH_TYPE_FULLSCREEN, 62 LAUNCH_TYPE_WINDOW, 63 LAUNCH_TYPE_LAST, 64}; 65 66// ExtensionUninstaller decouples ExtensionAppItem from the extension uninstall 67// flow. It shows extension uninstall dialog and wait for user to confirm or 68// cancel the uninstall. 69class ExtensionUninstaller : public ExtensionUninstallDialog::Delegate { 70 public: 71 ExtensionUninstaller(Profile* profile, 72 const std::string& extension_id, 73 AppListControllerDelegate* controller) 74 : profile_(profile), 75 extension_id_(extension_id), 76 controller_(controller) { 77 } 78 79 void Run() { 80 const Extension* extension = 81 extensions::ExtensionSystem::Get(profile_)->extension_service()-> 82 GetExtensionById(extension_id_, true); 83 if (!extension) { 84 CleanUp(); 85 return; 86 } 87 controller_->OnShowExtensionPrompt(); 88 dialog_.reset(ExtensionUninstallDialog::Create(profile_, NULL, this)); 89 dialog_->ConfirmUninstall(extension); 90 } 91 92 private: 93 // Overridden from ExtensionUninstallDialog::Delegate: 94 virtual void ExtensionUninstallAccepted() OVERRIDE { 95 ExtensionService* service = 96 extensions::ExtensionSystem::Get(profile_)->extension_service(); 97 const Extension* extension = service->GetInstalledExtension(extension_id_); 98 if (extension) { 99 service->UninstallExtension(extension_id_, 100 false, /* external_uninstall*/ 101 NULL); 102 } 103 controller_->OnCloseExtensionPrompt(); 104 CleanUp(); 105 } 106 107 virtual void ExtensionUninstallCanceled() OVERRIDE { 108 controller_->OnCloseExtensionPrompt(); 109 CleanUp(); 110 } 111 112 void CleanUp() { 113 delete this; 114 } 115 116 Profile* profile_; 117 std::string extension_id_; 118 AppListControllerDelegate* controller_; 119 scoped_ptr<ExtensionUninstallDialog> dialog_; 120 121 DISALLOW_COPY_AND_ASSIGN(ExtensionUninstaller); 122}; 123 124// Overlays a shortcut icon over the bottom left corner of a given image. 125class ShortcutOverlayImageSource : public gfx::CanvasImageSource { 126 public: 127 explicit ShortcutOverlayImageSource(const gfx::ImageSkia& icon) 128 : gfx::CanvasImageSource(icon.size(), false), 129 icon_(icon) { 130 } 131 virtual ~ShortcutOverlayImageSource() {} 132 133 private: 134 // gfx::CanvasImageSource overrides: 135 virtual void Draw(gfx::Canvas* canvas) OVERRIDE { 136 canvas->DrawImageInt(icon_, 0, 0); 137 138 // Draw the overlay in the bottom left corner of the icon. 139 const gfx::ImageSkia& overlay = *ui::ResourceBundle::GetSharedInstance(). 140 GetImageSkiaNamed(IDR_APP_LIST_TAB_OVERLAY); 141 canvas->DrawImageInt(overlay, 0, icon_.height() - overlay.height()); 142 } 143 144 gfx::ImageSkia icon_; 145 146 DISALLOW_COPY_AND_ASSIGN(ShortcutOverlayImageSource); 147}; 148 149extensions::ExtensionPrefs::LaunchType GetExtensionLaunchType( 150 Profile* profile, 151 const Extension* extension) { 152 ExtensionService* service = 153 extensions::ExtensionSystem::Get(profile)->extension_service(); 154 return service->extension_prefs()-> 155 GetLaunchType(extension, extensions::ExtensionPrefs::LAUNCH_DEFAULT); 156} 157 158void SetExtensionLaunchType( 159 Profile* profile, 160 const std::string& extension_id, 161 extensions::ExtensionPrefs::LaunchType launch_type) { 162 ExtensionService* service = 163 extensions::ExtensionSystem::Get(profile)->extension_service(); 164 service->extension_prefs()->SetLaunchType(extension_id, launch_type); 165} 166 167ExtensionSorting* GetExtensionSorting(Profile* profile) { 168 ExtensionService* service = 169 extensions::ExtensionSystem::Get(profile)->extension_service(); 170 return service->extension_prefs()->extension_sorting(); 171} 172 173bool MenuItemHasLauncherContext(const extensions::MenuItem* item) { 174 return item->contexts().Contains(extensions::MenuItem::LAUNCHER); 175} 176 177const color_utils::HSL shift = {-1, 0, 0.6}; 178 179} // namespace 180 181ExtensionAppItem::ExtensionAppItem(Profile* profile, 182 const std::string& extension_id, 183 AppListControllerDelegate* controller, 184 const std::string& extension_name, 185 const gfx::ImageSkia& installing_icon, 186 bool is_platform_app) 187 : ChromeAppListItem(TYPE_APP), 188 profile_(profile), 189 extension_id_(extension_id), 190 controller_(controller), 191 extension_name_(extension_name), 192 installing_icon_( 193 gfx::ImageSkiaOperations::CreateHSLShiftedImage(installing_icon, 194 shift)), 195 is_platform_app_(is_platform_app) { 196 Reload(); 197 GetExtensionSorting(profile_)->EnsureValidOrdinals(extension_id_, 198 syncer::StringOrdinal()); 199} 200 201ExtensionAppItem::~ExtensionAppItem() { 202} 203 204bool ExtensionAppItem::HasOverlay() const { 205#if defined(OS_CHROMEOS) 206 return false; 207#else 208 return !is_platform_app_ && extension_id_ != extension_misc::kChromeAppId; 209#endif 210} 211 212void ExtensionAppItem::Reload() { 213 const Extension* extension = GetExtension(); 214 bool is_installing = !extension; 215 SetIsInstalling(is_installing); 216 set_app_id(extension_id_); 217 if (is_installing) { 218 SetTitle(extension_name_); 219 UpdateIcon(); 220 return; 221 } 222 SetTitle(extension->name()); 223 LoadImage(extension); 224} 225 226syncer::StringOrdinal ExtensionAppItem::GetPageOrdinal() const { 227 return GetExtensionSorting(profile_)->GetPageOrdinal(extension_id_); 228} 229 230syncer::StringOrdinal ExtensionAppItem::GetAppLaunchOrdinal() const { 231 return GetExtensionSorting(profile_)->GetAppLaunchOrdinal(extension_id_); 232} 233 234void ExtensionAppItem::Move(const ExtensionAppItem* prev, 235 const ExtensionAppItem* next) { 236 // Does nothing if no predecessor nor successor. 237 if (!prev && !next) 238 return; 239 240 ExtensionService* service = 241 extensions::ExtensionSystem::Get(profile_)->extension_service(); 242 service->extension_prefs()->SetAppDraggedByUser(extension_id_); 243 244 // Handles only predecessor or only successor case. 245 if (!prev || !next) { 246 syncer::StringOrdinal page = prev ? prev->GetPageOrdinal() : 247 next->GetPageOrdinal(); 248 GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, page); 249 service->OnExtensionMoved(extension_id_, 250 prev ? prev->extension_id() : std::string(), 251 next ? next->extension_id() : std::string()); 252 return; 253 } 254 255 // Handles both predecessor and successor are on the same page. 256 syncer::StringOrdinal prev_page = prev->GetPageOrdinal(); 257 syncer::StringOrdinal next_page = next->GetPageOrdinal(); 258 if (prev_page.Equals(next_page)) { 259 GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, prev_page); 260 service->OnExtensionMoved(extension_id_, 261 prev->extension_id(), 262 next->extension_id()); 263 return; 264 } 265 266 // Otherwise, go with |next|. This is okay because app list does not split 267 // page based ntp page ordinal. 268 // TODO(xiyuan): Revisit this when implementing paging support. 269 GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, prev_page); 270 service->OnExtensionMoved(extension_id_, 271 prev->extension_id(), 272 std::string()); 273} 274 275void ExtensionAppItem::UpdateIcon() { 276 if (!GetExtension()) { 277 gfx::ImageSkia icon = installing_icon_; 278 if (HasOverlay()) 279 icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size()); 280 SetIcon(icon, !HasOverlay()); 281 return; 282 } 283 gfx::ImageSkia icon = icon_->image_skia(); 284 285 const ExtensionService* service = 286 extensions::ExtensionSystem::Get(profile_)->extension_service(); 287 const bool enabled = service->IsExtensionEnabledForLauncher(extension_id_); 288 if (!enabled) { 289 const color_utils::HSL shift = {-1, 0, 0.6}; 290 icon = gfx::ImageSkiaOperations::CreateHSLShiftedImage(icon, shift); 291 } 292 293 if (HasOverlay()) 294 icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size()); 295 296 SetIcon(icon, !HasOverlay()); 297} 298 299const Extension* ExtensionAppItem::GetExtension() const { 300 const ExtensionService* service = 301 extensions::ExtensionSystem::Get(profile_)->extension_service(); 302 const Extension* extension = service->GetInstalledExtension(extension_id_); 303 return extension; 304} 305 306void ExtensionAppItem::LoadImage(const Extension* extension) { 307 icon_.reset(new extensions::IconImage( 308 profile_, 309 extension, 310 extensions::IconsInfo::GetIcons(extension), 311 extension_misc::EXTENSION_ICON_MEDIUM, 312 extensions::IconsInfo::GetDefaultAppIcon(), 313 this)); 314 UpdateIcon(); 315} 316 317void ExtensionAppItem::ShowExtensionOptions() { 318 const Extension* extension = GetExtension(); 319 if (!extension) 320 return; 321 322 chrome::NavigateParams params( 323 profile_, 324 extensions::ManifestURL::GetOptionsPage(extension), 325 content::PAGE_TRANSITION_LINK); 326 chrome::Navigate(¶ms); 327} 328 329void ExtensionAppItem::ShowExtensionDetails() { 330 const Extension* extension = GetExtension(); 331 if (!extension) 332 return; 333 334 chrome::NavigateParams params( 335 profile_, 336 extensions::ManifestURL::GetDetailsURL(extension), 337 content::PAGE_TRANSITION_LINK); 338 chrome::Navigate(¶ms); 339} 340 341void ExtensionAppItem::StartExtensionUninstall() { 342 // ExtensionUninstall deletes itself when done or aborted. 343 ExtensionUninstaller* uninstaller = new ExtensionUninstaller(profile_, 344 extension_id_, 345 controller_); 346 uninstaller->Run(); 347} 348 349bool ExtensionAppItem::RunExtensionEnableFlow() { 350 const ExtensionService* service = 351 extensions::ExtensionSystem::Get(profile_)->extension_service(); 352 if (service->IsExtensionEnabledForLauncher(extension_id_)) 353 return false; 354 355 if (!extension_enable_flow_) { 356 controller_->OnShowExtensionPrompt(); 357 358 extension_enable_flow_.reset(new ExtensionEnableFlow( 359 profile_, extension_id_, this)); 360 extension_enable_flow_->StartForNativeWindow( 361 controller_->GetAppListWindow()); 362 } 363 return true; 364} 365 366void ExtensionAppItem::Launch(int event_flags) { 367 // |extension| could be NULL when it is being unloaded for updating. 368 const Extension* extension = GetExtension(); 369 if (!extension) 370 return; 371 372 if (RunExtensionEnableFlow()) 373 return; 374 375 controller_->LaunchApp(profile_, extension, event_flags); 376} 377 378void ExtensionAppItem::OnExtensionIconImageChanged( 379 extensions::IconImage* image) { 380 DCHECK(icon_.get() == image); 381 UpdateIcon(); 382} 383 384void ExtensionAppItem::ExtensionEnableFlowFinished() { 385 extension_enable_flow_.reset(); 386 controller_->OnCloseExtensionPrompt(); 387 388 // Automatically launch app after enabling. 389 Launch(ui::EF_NONE); 390} 391 392void ExtensionAppItem::ExtensionEnableFlowAborted(bool user_initiated) { 393 extension_enable_flow_.reset(); 394 controller_->OnCloseExtensionPrompt(); 395} 396 397bool ExtensionAppItem::IsItemForCommandIdDynamic(int command_id) const { 398 return command_id == TOGGLE_PIN || command_id == LAUNCH_NEW; 399} 400 401string16 ExtensionAppItem::GetLabelForCommandId(int command_id) const { 402 if (command_id == TOGGLE_PIN) { 403 return controller_->IsAppPinned(extension_id_) ? 404 l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_UNPIN) : 405 l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_PIN); 406 } else if (command_id == LAUNCH_NEW) { 407 if (IsCommandIdChecked(LAUNCH_TYPE_PINNED_TAB) || 408 IsCommandIdChecked(LAUNCH_TYPE_REGULAR_TAB)) { 409 return l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_NEW_TAB); 410 } else { 411 return l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW); 412 } 413 } else { 414 NOTREACHED(); 415 return string16(); 416 } 417} 418 419bool ExtensionAppItem::IsCommandIdChecked(int command_id) const { 420 if (command_id >= LAUNCH_TYPE_START && command_id < LAUNCH_TYPE_LAST) { 421 return static_cast<int>(GetExtensionLaunchType(profile_, GetExtension())) + 422 LAUNCH_TYPE_START == command_id; 423 } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && 424 command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { 425 return extension_menu_items_->IsCommandIdChecked(command_id); 426 } 427 return false; 428} 429 430bool ExtensionAppItem::IsCommandIdEnabled(int command_id) const { 431 if (command_id == TOGGLE_PIN) { 432 return controller_->CanPin(); 433 } else if (command_id == OPTIONS) { 434 const ExtensionService* service = 435 extensions::ExtensionSystem::Get(profile_)->extension_service(); 436 const Extension* extension = GetExtension(); 437 return service->IsExtensionEnabledForLauncher(extension_id_) && 438 extension && 439 !extensions::ManifestURL::GetOptionsPage(extension).is_empty(); 440 } else if (command_id == UNINSTALL) { 441 const Extension* extension = GetExtension(); 442 const extensions::ManagementPolicy* policy = 443 extensions::ExtensionSystem::Get(profile_)->management_policy(); 444 return extension && 445 policy->UserMayModifySettings(extension, NULL); 446 } else if (command_id == DETAILS) { 447 const Extension* extension = GetExtension(); 448 return extension && extension->from_webstore(); 449 } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && 450 command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { 451 return extension_menu_items_->IsCommandIdEnabled(command_id); 452 } else if (command_id == MENU_NEW_WINDOW) { 453 // "Normal" windows are not allowed when incognito is enforced. 454 return IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) != 455 IncognitoModePrefs::FORCED; 456 } else if (command_id == MENU_NEW_INCOGNITO_WINDOW) { 457 // Incognito windows are not allowed when incognito is disabled. 458 return IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) != 459 IncognitoModePrefs::DISABLED; 460 } 461 return true; 462} 463 464bool ExtensionAppItem::GetAcceleratorForCommandId( 465 int command_id, 466 ui::Accelerator* acclelrator) { 467 return false; 468} 469 470void ExtensionAppItem::ExecuteCommand(int command_id, int event_flags) { 471 if (command_id == LAUNCH_NEW) { 472 Launch(ui::EF_NONE); 473 } else if (command_id == TOGGLE_PIN && controller_->CanPin()) { 474 if (controller_->IsAppPinned(extension_id_)) 475 controller_->UnpinApp(extension_id_); 476 else 477 controller_->PinApp(extension_id_); 478 } else if (command_id == CREATE_SHORTCUTS) { 479 controller_->ShowCreateShortcutsDialog(profile_, extension_id_); 480 } else if (command_id >= LAUNCH_TYPE_START && 481 command_id < LAUNCH_TYPE_LAST) { 482 SetExtensionLaunchType(profile_, 483 extension_id_, 484 static_cast<extensions::ExtensionPrefs::LaunchType>( 485 command_id - LAUNCH_TYPE_START)); 486 } else if (command_id == OPTIONS) { 487 ShowExtensionOptions(); 488 } else if (command_id == UNINSTALL) { 489 StartExtensionUninstall(); 490 } else if (command_id == DETAILS) { 491 ShowExtensionDetails(); 492 } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && 493 command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { 494 extension_menu_items_->ExecuteCommand(command_id, NULL, 495 content::ContextMenuParams()); 496 } else if (command_id == MENU_NEW_WINDOW) { 497 controller_->CreateNewWindow(profile_, false); 498 } else if (command_id == MENU_NEW_INCOGNITO_WINDOW) { 499 controller_->CreateNewWindow(profile_, true); 500 } 501} 502 503void ExtensionAppItem::Activate(int event_flags) { 504 // |extension| could be NULL when it is being unloaded for updating. 505 const Extension* extension = GetExtension(); 506 if (!extension) 507 return; 508 509 if (RunExtensionEnableFlow()) 510 return; 511 512 AppLauncherHandler::RecordAppLaunchType( 513 extension_misc::APP_LAUNCH_APP_LIST_MAIN, 514 extension->GetType()); 515 controller_->ActivateApp(profile_, extension, event_flags); 516} 517 518ui::MenuModel* ExtensionAppItem::GetContextMenuModel() { 519 const Extension* extension = GetExtension(); 520 if (!extension) 521 return NULL; 522 523 if (context_menu_model_.get()) 524 return context_menu_model_.get(); 525 526 context_menu_model_.reset(new ui::SimpleMenuModel(this)); 527 528 if (extension_id_ == extension_misc::kChromeAppId) { 529 context_menu_model_->AddItemWithStringId( 530 MENU_NEW_WINDOW, 531 IDS_APP_LIST_NEW_WINDOW); 532 if (!profile_->IsOffTheRecord()) { 533 context_menu_model_->AddItemWithStringId( 534 MENU_NEW_INCOGNITO_WINDOW, 535 IDS_APP_LIST_NEW_INCOGNITO_WINDOW); 536 } 537 } else { 538 extension_menu_items_.reset(new extensions::ContextMenuMatcher( 539 profile_, this, context_menu_model_.get(), 540 base::Bind(MenuItemHasLauncherContext))); 541 542 if (!is_platform_app_) 543 context_menu_model_->AddItem(LAUNCH_NEW, string16()); 544 545 int index = 0; 546 extension_menu_items_->AppendExtensionItems(extension_id_, string16(), 547 &index); 548 549 if (controller_->CanPin()) { 550 context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); 551 context_menu_model_->AddItemWithStringId( 552 TOGGLE_PIN, 553 controller_->IsAppPinned(extension_id_) ? 554 IDS_APP_LIST_CONTEXT_MENU_UNPIN : 555 IDS_APP_LIST_CONTEXT_MENU_PIN); 556 } 557 558 if (controller_->CanShowCreateShortcutsDialog()) { 559 context_menu_model_->AddItemWithStringId(CREATE_SHORTCUTS, 560 IDS_NEW_TAB_APP_CREATE_SHORTCUT); 561 } 562 563 if (!is_platform_app_) { 564 context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); 565 context_menu_model_->AddCheckItemWithStringId( 566 LAUNCH_TYPE_REGULAR_TAB, 567 IDS_APP_CONTEXT_MENU_OPEN_REGULAR); 568 context_menu_model_->AddCheckItemWithStringId( 569 LAUNCH_TYPE_PINNED_TAB, 570 IDS_APP_CONTEXT_MENU_OPEN_PINNED); 571#if defined(USE_ASH) 572 if (!ash::Shell::IsForcedMaximizeMode()) 573#endif 574 { 575 context_menu_model_->AddCheckItemWithStringId( 576 LAUNCH_TYPE_WINDOW, 577 IDS_APP_CONTEXT_MENU_OPEN_WINDOW); 578 // Even though the launch type is Full Screen it is more accurately 579 // described as Maximized in Ash. 580 context_menu_model_->AddCheckItemWithStringId( 581 LAUNCH_TYPE_FULLSCREEN, 582 IDS_APP_CONTEXT_MENU_OPEN_MAXIMIZED); 583 } 584 context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); 585 context_menu_model_->AddItemWithStringId(OPTIONS, 586 IDS_NEW_TAB_APP_OPTIONS); 587 } 588 589 context_menu_model_->AddItemWithStringId(DETAILS, 590 IDS_NEW_TAB_APP_DETAILS); 591 context_menu_model_->AddItemWithStringId(UNINSTALL, 592 is_platform_app_ ? 593 IDS_APP_LIST_UNINSTALL_ITEM : 594 IDS_EXTENSIONS_UNINSTALL); 595 } 596 597 return context_menu_model_.get(); 598} 599