extension_app_item.cc revision b2df76ea8fec9e32f6f3718986dba0d95315b29c
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 return !is_platform_app_ && extension_id_ != extension_misc::kChromeAppId; 206} 207 208void ExtensionAppItem::Reload() { 209 const Extension* extension = GetExtension(); 210 bool is_installing = !extension; 211 SetIsInstalling(is_installing); 212 set_app_id(extension_id_); 213 if (is_installing) { 214 SetTitle(extension_name_); 215 UpdateIcon(); 216 return; 217 } 218 SetTitle(extension->name()); 219 LoadImage(extension); 220} 221 222syncer::StringOrdinal ExtensionAppItem::GetPageOrdinal() const { 223 return GetExtensionSorting(profile_)->GetPageOrdinal(extension_id_); 224} 225 226syncer::StringOrdinal ExtensionAppItem::GetAppLaunchOrdinal() const { 227 return GetExtensionSorting(profile_)->GetAppLaunchOrdinal(extension_id_); 228} 229 230void ExtensionAppItem::Move(const ExtensionAppItem* prev, 231 const ExtensionAppItem* next) { 232 // Does nothing if no predecessor nor successor. 233 if (!prev && !next) 234 return; 235 236 ExtensionService* service = 237 extensions::ExtensionSystem::Get(profile_)->extension_service(); 238 service->extension_prefs()->SetAppDraggedByUser(extension_id_); 239 240 // Handles only predecessor or only successor case. 241 if (!prev || !next) { 242 syncer::StringOrdinal page = prev ? prev->GetPageOrdinal() : 243 next->GetPageOrdinal(); 244 GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, page); 245 service->OnExtensionMoved(extension_id_, 246 prev ? prev->extension_id() : std::string(), 247 next ? next->extension_id() : std::string()); 248 return; 249 } 250 251 // Handles both predecessor and successor are on the same page. 252 syncer::StringOrdinal prev_page = prev->GetPageOrdinal(); 253 syncer::StringOrdinal next_page = next->GetPageOrdinal(); 254 if (prev_page.Equals(next_page)) { 255 GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, prev_page); 256 service->OnExtensionMoved(extension_id_, 257 prev->extension_id(), 258 next->extension_id()); 259 return; 260 } 261 262 // Otherwise, go with |next|. This is okay because app list does not split 263 // page based ntp page ordinal. 264 // TODO(xiyuan): Revisit this when implementing paging support. 265 GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, prev_page); 266 service->OnExtensionMoved(extension_id_, 267 prev->extension_id(), 268 std::string()); 269} 270 271void ExtensionAppItem::UpdateIcon() { 272 if (!GetExtension()) { 273 gfx::ImageSkia icon = installing_icon_; 274 if (HasOverlay()) 275 icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size()); 276 SetIcon(icon, !HasOverlay()); 277 return; 278 } 279 gfx::ImageSkia icon = icon_->image_skia(); 280 281 const ExtensionService* service = 282 extensions::ExtensionSystem::Get(profile_)->extension_service(); 283 const bool enabled = service->IsExtensionEnabledForLauncher(extension_id_); 284 if (!enabled) { 285 const color_utils::HSL shift = {-1, 0, 0.6}; 286 icon = gfx::ImageSkiaOperations::CreateHSLShiftedImage(icon, shift); 287 } 288 289 if (HasOverlay()) 290 icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size()); 291 292 SetIcon(icon, !HasOverlay()); 293} 294 295const Extension* ExtensionAppItem::GetExtension() const { 296 const ExtensionService* service = 297 extensions::ExtensionSystem::Get(profile_)->extension_service(); 298 const Extension* extension = service->GetInstalledExtension(extension_id_); 299 return extension; 300} 301 302void ExtensionAppItem::LoadImage(const Extension* extension) { 303 icon_.reset(new extensions::IconImage( 304 profile_, 305 extension, 306 extensions::IconsInfo::GetIcons(extension), 307 extension_misc::EXTENSION_ICON_MEDIUM, 308 extensions::IconsInfo::GetDefaultAppIcon(), 309 this)); 310 UpdateIcon(); 311} 312 313void ExtensionAppItem::ShowExtensionOptions() { 314 const Extension* extension = GetExtension(); 315 if (!extension) 316 return; 317 318 chrome::NavigateParams params( 319 profile_, 320 extensions::ManifestURL::GetOptionsPage(extension), 321 content::PAGE_TRANSITION_LINK); 322 chrome::Navigate(¶ms); 323} 324 325void ExtensionAppItem::ShowExtensionDetails() { 326 const Extension* extension = GetExtension(); 327 if (!extension) 328 return; 329 330 chrome::NavigateParams params( 331 profile_, 332 extensions::ManifestURL::GetDetailsURL(extension), 333 content::PAGE_TRANSITION_LINK); 334 chrome::Navigate(¶ms); 335} 336 337void ExtensionAppItem::StartExtensionUninstall() { 338 // ExtensionUninstall deletes itself when done or aborted. 339 ExtensionUninstaller* uninstaller = new ExtensionUninstaller(profile_, 340 extension_id_, 341 controller_); 342 uninstaller->Run(); 343} 344 345bool ExtensionAppItem::RunExtensionEnableFlow() { 346 const ExtensionService* service = 347 extensions::ExtensionSystem::Get(profile_)->extension_service(); 348 if (service->IsExtensionEnabledForLauncher(extension_id_)) 349 return false; 350 351 if (!extension_enable_flow_) { 352 controller_->OnShowExtensionPrompt(); 353 354 extension_enable_flow_.reset(new ExtensionEnableFlow( 355 profile_, extension_id_, this)); 356 extension_enable_flow_->StartForNativeWindow( 357 controller_->GetAppListWindow()); 358 } 359 return true; 360} 361 362void ExtensionAppItem::Launch(int event_flags) { 363 // |extension| could be NULL when it is being unloaded for updating. 364 const Extension* extension = GetExtension(); 365 if (!extension) 366 return; 367 368 if (RunExtensionEnableFlow()) 369 return; 370 371 controller_->LaunchApp(profile_, extension, event_flags); 372} 373 374void ExtensionAppItem::OnExtensionIconImageChanged( 375 extensions::IconImage* image) { 376 DCHECK(icon_.get() == image); 377 UpdateIcon(); 378} 379 380void ExtensionAppItem::ExtensionEnableFlowFinished() { 381 extension_enable_flow_.reset(); 382 controller_->OnCloseExtensionPrompt(); 383 384 // Automatically launch app after enabling. 385 Launch(ui::EF_NONE); 386} 387 388void ExtensionAppItem::ExtensionEnableFlowAborted(bool user_initiated) { 389 extension_enable_flow_.reset(); 390 controller_->OnCloseExtensionPrompt(); 391} 392 393bool ExtensionAppItem::IsItemForCommandIdDynamic(int command_id) const { 394 return command_id == TOGGLE_PIN || command_id == LAUNCH_NEW; 395} 396 397string16 ExtensionAppItem::GetLabelForCommandId(int command_id) const { 398 if (command_id == TOGGLE_PIN) { 399 return controller_->IsAppPinned(extension_id_) ? 400 l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_UNPIN) : 401 l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_PIN); 402 } else if (command_id == LAUNCH_NEW) { 403 if (IsCommandIdChecked(LAUNCH_TYPE_PINNED_TAB) || 404 IsCommandIdChecked(LAUNCH_TYPE_REGULAR_TAB)) { 405 return l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_NEW_TAB); 406 } else { 407 return l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW); 408 } 409 } else { 410 NOTREACHED(); 411 return string16(); 412 } 413} 414 415bool ExtensionAppItem::IsCommandIdChecked(int command_id) const { 416 if (command_id >= LAUNCH_TYPE_START && command_id < LAUNCH_TYPE_LAST) { 417 return static_cast<int>(GetExtensionLaunchType(profile_, GetExtension())) + 418 LAUNCH_TYPE_START == command_id; 419 } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && 420 command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { 421 return extension_menu_items_->IsCommandIdChecked(command_id); 422 } 423 return false; 424} 425 426bool ExtensionAppItem::IsCommandIdEnabled(int command_id) const { 427 if (command_id == TOGGLE_PIN) { 428 return controller_->CanPin(); 429 } else if (command_id == OPTIONS) { 430 const ExtensionService* service = 431 extensions::ExtensionSystem::Get(profile_)->extension_service(); 432 const Extension* extension = GetExtension(); 433 return service->IsExtensionEnabledForLauncher(extension_id_) && 434 extension && 435 !extensions::ManifestURL::GetOptionsPage(extension).is_empty(); 436 } else if (command_id == UNINSTALL) { 437 const Extension* extension = GetExtension(); 438 const extensions::ManagementPolicy* policy = 439 extensions::ExtensionSystem::Get(profile_)->management_policy(); 440 return extension && 441 policy->UserMayModifySettings(extension, NULL); 442 } else if (command_id == DETAILS) { 443 const Extension* extension = GetExtension(); 444 return extension && extension->from_webstore(); 445 } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && 446 command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { 447 return extension_menu_items_->IsCommandIdEnabled(command_id); 448 } else if (command_id == MENU_NEW_WINDOW) { 449 // "Normal" windows are not allowed when incognito is enforced. 450 return IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) != 451 IncognitoModePrefs::FORCED; 452 } else if (command_id == MENU_NEW_INCOGNITO_WINDOW) { 453 // Incognito windows are not allowed when incognito is disabled. 454 return IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) != 455 IncognitoModePrefs::DISABLED; 456 } 457 return true; 458} 459 460bool ExtensionAppItem::GetAcceleratorForCommandId( 461 int command_id, 462 ui::Accelerator* acclelrator) { 463 return false; 464} 465 466void ExtensionAppItem::ExecuteCommand(int command_id, int event_flags) { 467 if (command_id == LAUNCH_NEW) { 468 Launch(ui::EF_NONE); 469 } else if (command_id == TOGGLE_PIN && controller_->CanPin()) { 470 if (controller_->IsAppPinned(extension_id_)) 471 controller_->UnpinApp(extension_id_); 472 else 473 controller_->PinApp(extension_id_); 474 } else if (command_id == CREATE_SHORTCUTS) { 475 controller_->ShowCreateShortcutsDialog(profile_, extension_id_); 476 } else if (command_id >= LAUNCH_TYPE_START && 477 command_id < LAUNCH_TYPE_LAST) { 478 SetExtensionLaunchType(profile_, 479 extension_id_, 480 static_cast<extensions::ExtensionPrefs::LaunchType>( 481 command_id - LAUNCH_TYPE_START)); 482 } else if (command_id == OPTIONS) { 483 ShowExtensionOptions(); 484 } else if (command_id == UNINSTALL) { 485 StartExtensionUninstall(); 486 } else if (command_id == DETAILS) { 487 ShowExtensionDetails(); 488 } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && 489 command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { 490 extension_menu_items_->ExecuteCommand(command_id, NULL, 491 content::ContextMenuParams()); 492 } else if (command_id == MENU_NEW_WINDOW) { 493 controller_->CreateNewWindow(profile_, false); 494 } else if (command_id == MENU_NEW_INCOGNITO_WINDOW) { 495 controller_->CreateNewWindow(profile_, true); 496 } 497} 498 499void ExtensionAppItem::Activate(int event_flags) { 500 // |extension| could be NULL when it is being unloaded for updating. 501 const Extension* extension = GetExtension(); 502 if (!extension) 503 return; 504 505 if (RunExtensionEnableFlow()) 506 return; 507 508 AppLauncherHandler::RecordAppLaunchType( 509 extension_misc::APP_LAUNCH_APP_LIST_MAIN, 510 extension->GetType()); 511 controller_->ActivateApp(profile_, extension, event_flags); 512} 513 514ui::MenuModel* ExtensionAppItem::GetContextMenuModel() { 515 const Extension* extension = GetExtension(); 516 if (!extension) 517 return NULL; 518 519 if (context_menu_model_.get()) 520 return context_menu_model_.get(); 521 522 context_menu_model_.reset(new ui::SimpleMenuModel(this)); 523 524 if (extension_id_ == extension_misc::kChromeAppId) { 525 context_menu_model_->AddItemWithStringId( 526 MENU_NEW_WINDOW, 527 IDS_APP_LIST_NEW_WINDOW); 528 if (!profile_->IsOffTheRecord()) { 529 context_menu_model_->AddItemWithStringId( 530 MENU_NEW_INCOGNITO_WINDOW, 531 IDS_APP_LIST_NEW_INCOGNITO_WINDOW); 532 } 533 } else { 534 extension_menu_items_.reset(new extensions::ContextMenuMatcher( 535 profile_, this, context_menu_model_.get(), 536 base::Bind(MenuItemHasLauncherContext))); 537 538 if (!is_platform_app_) 539 context_menu_model_->AddItem(LAUNCH_NEW, string16()); 540 541 int index = 0; 542 extension_menu_items_->AppendExtensionItems(extension_id_, string16(), 543 &index); 544 545 if (controller_->CanPin()) { 546 context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); 547 context_menu_model_->AddItemWithStringId( 548 TOGGLE_PIN, 549 controller_->IsAppPinned(extension_id_) ? 550 IDS_APP_LIST_CONTEXT_MENU_UNPIN : 551 IDS_APP_LIST_CONTEXT_MENU_PIN); 552 } 553 554 if (controller_->CanShowCreateShortcutsDialog()) { 555 context_menu_model_->AddItemWithStringId(CREATE_SHORTCUTS, 556 IDS_NEW_TAB_APP_CREATE_SHORTCUT); 557 } 558 559 if (!is_platform_app_) { 560 context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); 561 context_menu_model_->AddCheckItemWithStringId( 562 LAUNCH_TYPE_REGULAR_TAB, 563 IDS_APP_CONTEXT_MENU_OPEN_REGULAR); 564 context_menu_model_->AddCheckItemWithStringId( 565 LAUNCH_TYPE_PINNED_TAB, 566 IDS_APP_CONTEXT_MENU_OPEN_PINNED); 567#if defined(USE_ASH) 568 if (!ash::Shell::IsForcedMaximizeMode()) 569#endif 570 { 571 context_menu_model_->AddCheckItemWithStringId( 572 LAUNCH_TYPE_WINDOW, 573 IDS_APP_CONTEXT_MENU_OPEN_WINDOW); 574 // Even though the launch type is Full Screen it is more accurately 575 // described as Maximized in Ash. 576 context_menu_model_->AddCheckItemWithStringId( 577 LAUNCH_TYPE_FULLSCREEN, 578 IDS_APP_CONTEXT_MENU_OPEN_MAXIMIZED); 579 } 580 context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); 581 context_menu_model_->AddItemWithStringId(OPTIONS, 582 IDS_NEW_TAB_APP_OPTIONS); 583 } 584 585 context_menu_model_->AddItemWithStringId(DETAILS, 586 IDS_NEW_TAB_APP_DETAILS); 587 context_menu_model_->AddItemWithStringId(UNINSTALL, 588 is_platform_app_ ? 589 IDS_APP_LIST_UNINSTALL_ITEM : 590 IDS_EXTENSIONS_UNINSTALL); 591 } 592 593 return context_menu_model_.get(); 594} 595