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/views/create_application_shortcut_view.h" 6 7 #include <algorithm> 8 #include <cmath> 9 10 #include "base/bind.h" 11 #include "base/bind_helpers.h" 12 #include "base/prefs/pref_service.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "base/win/windows_version.h" 15 #include "chrome/browser/extensions/tab_helper.h" 16 #include "chrome/browser/profiles/profile.h" 17 #include "chrome/browser/ui/browser.h" 18 #include "chrome/browser/ui/browser_commands.h" 19 #include "chrome/browser/ui/browser_finder.h" 20 #include "chrome/browser/ui/views/constrained_window_views.h" 21 #include "chrome/browser/ui/webui/extensions/extension_icon_source.h" 22 #include "chrome/browser/web_applications/web_app.h" 23 #include "chrome/common/chrome_constants.h" 24 #include "chrome/common/pref_names.h" 25 #include "components/favicon_base/select_favicon_frames.h" 26 #include "content/public/browser/render_view_host.h" 27 #include "content/public/browser/render_widget_host_view.h" 28 #include "content/public/browser/web_contents.h" 29 #include "extensions/common/extension.h" 30 #include "grit/chromium_strings.h" 31 #include "grit/generated_resources.h" 32 #include "grit/locale_settings.h" 33 #include "grit/theme_resources.h" 34 #include "net/base/load_flags.h" 35 #include "net/url_request/url_request.h" 36 #include "skia/ext/image_operations.h" 37 #include "third_party/skia/include/core/SkBitmap.h" 38 #include "third_party/skia/include/core/SkPaint.h" 39 #include "third_party/skia/include/core/SkRect.h" 40 #include "ui/base/l10n/l10n_util.h" 41 #include "ui/base/layout.h" 42 #include "ui/base/resource/resource_bundle.h" 43 #include "ui/gfx/canvas.h" 44 #include "ui/gfx/codec/png_codec.h" 45 #include "ui/gfx/image/image_family.h" 46 #include "ui/gfx/image/image_skia.h" 47 #include "ui/views/controls/button/checkbox.h" 48 #include "ui/views/controls/image_view.h" 49 #include "ui/views/controls/label.h" 50 #include "ui/views/layout/grid_layout.h" 51 #include "ui/views/layout/layout_constants.h" 52 #include "ui/views/widget/widget.h" 53 #include "ui/views/window/dialog_client_view.h" 54 #include "url/gurl.h" 55 56 namespace { 57 58 const int kIconPreviewSizePixels = 32; 59 60 // AppInfoView shows the application icon and title. 61 class AppInfoView : public views::View { 62 public: 63 AppInfoView(const base::string16& title, 64 const base::string16& description, 65 const gfx::ImageFamily& icon); 66 67 // Updates the title/description of the web app. 68 void UpdateText(const base::string16& title, 69 const base::string16& description); 70 71 // Updates the icon of the web app. 72 void UpdateIcon(const gfx::ImageFamily& image); 73 74 // Overridden from views::View: 75 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; 76 77 private: 78 // Initializes the controls 79 void Init(const base::string16& title, 80 const base::string16& description, const gfx::ImageFamily& icon); 81 82 // Creates or updates description label. 83 void PrepareDescriptionLabel(const base::string16& description); 84 85 // Sets up layout manager. 86 void SetupLayout(); 87 88 views::ImageView* icon_; 89 views::Label* title_; 90 views::Label* description_; 91 }; 92 93 AppInfoView::AppInfoView(const base::string16& title, 94 const base::string16& description, 95 const gfx::ImageFamily& icon) 96 : icon_(NULL), 97 title_(NULL), 98 description_(NULL) { 99 Init(title, description, icon); 100 } 101 102 void AppInfoView::Init(const base::string16& title_text, 103 const base::string16& description_text, 104 const gfx::ImageFamily& icon) { 105 icon_ = new views::ImageView(); 106 UpdateIcon(icon); 107 icon_->SetImageSize(gfx::Size(kIconPreviewSizePixels, 108 kIconPreviewSizePixels)); 109 110 title_ = new views::Label( 111 title_text, 112 ui::ResourceBundle::GetSharedInstance().GetFontList( 113 ui::ResourceBundle::BoldFont)); 114 title_->SetMultiLine(true); 115 title_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 116 117 PrepareDescriptionLabel(description_text); 118 119 SetupLayout(); 120 } 121 122 void AppInfoView::PrepareDescriptionLabel(const base::string16& description) { 123 // Do not make space for the description if it is empty. 124 if (description.empty()) 125 return; 126 127 const size_t kMaxLength = 200; 128 const base::string16 kEllipsis(base::ASCIIToUTF16(" ... ")); 129 130 base::string16 text = description; 131 if (text.length() > kMaxLength) { 132 text = text.substr(0, kMaxLength); 133 text += kEllipsis; 134 } 135 136 if (description_) { 137 description_->SetText(text); 138 } else { 139 description_ = new views::Label(text); 140 description_->SetMultiLine(true); 141 description_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 142 } 143 } 144 145 void AppInfoView::SetupLayout() { 146 views::GridLayout* layout = views::GridLayout::CreatePanel(this); 147 SetLayoutManager(layout); 148 149 static const int kColumnSetId = 0; 150 views::ColumnSet* column_set = layout->AddColumnSet(kColumnSetId); 151 column_set->AddColumn(views::GridLayout::CENTER, views::GridLayout::LEADING, 152 20.0f, views::GridLayout::FIXED, 153 kIconPreviewSizePixels, kIconPreviewSizePixels); 154 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER, 155 80.0f, views::GridLayout::USE_PREF, 0, 0); 156 157 layout->StartRow(0, kColumnSetId); 158 layout->AddView(icon_, 1, description_ ? 2 : 1); 159 layout->AddView(title_); 160 161 if (description_) { 162 layout->StartRow(0, kColumnSetId); 163 layout->SkipColumns(1); 164 layout->AddView(description_); 165 } 166 } 167 168 void AppInfoView::UpdateText(const base::string16& title, 169 const base::string16& description) { 170 title_->SetText(title); 171 PrepareDescriptionLabel(description); 172 173 SetupLayout(); 174 } 175 176 void AppInfoView::UpdateIcon(const gfx::ImageFamily& image) { 177 // Get the icon closest to the desired preview size. 178 const gfx::Image* icon = image.GetBest(kIconPreviewSizePixels, 179 kIconPreviewSizePixels); 180 if (!icon || icon->IsEmpty()) 181 // The family has no icons. Leave the image blank. 182 return; 183 const SkBitmap& bitmap = *icon->ToSkBitmap(); 184 if (bitmap.width() == kIconPreviewSizePixels && 185 bitmap.height() == kIconPreviewSizePixels) { 186 icon_->SetImage(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); 187 } else { 188 // Resize the image to the desired size. 189 SkBitmap resized_bitmap = skia::ImageOperations::Resize( 190 bitmap, skia::ImageOperations::RESIZE_LANCZOS3, 191 kIconPreviewSizePixels, kIconPreviewSizePixels); 192 193 icon_->SetImage(gfx::ImageSkia::CreateFrom1xBitmap(resized_bitmap)); 194 } 195 } 196 197 void AppInfoView::OnPaint(gfx::Canvas* canvas) { 198 gfx::Rect bounds = GetLocalBounds(); 199 200 SkRect border_rect = { 201 SkIntToScalar(bounds.x()), 202 SkIntToScalar(bounds.y()), 203 SkIntToScalar(bounds.right()), 204 SkIntToScalar(bounds.bottom()) 205 }; 206 207 SkPaint border_paint; 208 border_paint.setAntiAlias(true); 209 border_paint.setARGB(0xFF, 0xC8, 0xC8, 0xC8); 210 211 canvas->sk_canvas()->drawRoundRect(border_rect, SkIntToScalar(2), 212 SkIntToScalar(2), border_paint); 213 214 SkRect inner_rect = { 215 border_rect.fLeft + SkDoubleToScalar(0.5), 216 border_rect.fTop + SkDoubleToScalar(0.5), 217 border_rect.fRight - SkDoubleToScalar(0.5), 218 border_rect.fBottom - SkDoubleToScalar(0.5), 219 }; 220 221 SkPaint inner_paint; 222 inner_paint.setAntiAlias(true); 223 inner_paint.setARGB(0xFF, 0xF8, 0xF8, 0xF8); 224 canvas->sk_canvas()->drawRoundRect(inner_rect, SkDoubleToScalar(1.5), 225 SkDoubleToScalar(1.5), inner_paint); 226 } 227 228 } // namespace 229 230 namespace chrome { 231 232 void ShowCreateWebAppShortcutsDialog(gfx::NativeWindow parent_window, 233 content::WebContents* web_contents) { 234 CreateBrowserModalDialogViews( 235 new CreateUrlApplicationShortcutView(web_contents), 236 parent_window)->Show(); 237 } 238 239 void ShowCreateChromeAppShortcutsDialog( 240 gfx::NativeWindow parent_window, 241 Profile* profile, 242 const extensions::Extension* app, 243 const base::Callback<void(bool)>& close_callback) { 244 CreateBrowserModalDialogViews( 245 new CreateChromeApplicationShortcutView(profile, app, close_callback), 246 parent_window)->Show(); 247 } 248 249 } // namespace chrome 250 251 CreateApplicationShortcutView::CreateApplicationShortcutView(Profile* profile) 252 : profile_(profile), 253 app_info_(NULL), 254 create_shortcuts_label_(NULL), 255 desktop_check_box_(NULL), 256 menu_check_box_(NULL), 257 quick_launch_check_box_(NULL) {} 258 259 CreateApplicationShortcutView::~CreateApplicationShortcutView() {} 260 261 void CreateApplicationShortcutView::InitControls(DialogLayout dialog_layout) { 262 if (dialog_layout == DIALOG_LAYOUT_URL_SHORTCUT) { 263 app_info_ = new AppInfoView(shortcut_info_.title, 264 shortcut_info_.description, 265 shortcut_info_.favicon); 266 } 267 create_shortcuts_label_ = new views::Label( 268 l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_LABEL)); 269 create_shortcuts_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 270 271 desktop_check_box_ = AddCheckbox( 272 l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_DESKTOP_CHKBOX), 273 profile_->GetPrefs()->GetBoolean(prefs::kWebAppCreateOnDesktop)); 274 275 menu_check_box_ = NULL; 276 quick_launch_check_box_ = NULL; 277 278 #if defined(OS_WIN) 279 // Do not allow creating shortcuts on the Start Screen for Windows 8. 280 if (base::win::GetVersion() < base::win::VERSION_WIN8) { 281 menu_check_box_ = AddCheckbox( 282 l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_START_MENU_CHKBOX), 283 profile_->GetPrefs()->GetBoolean(prefs::kWebAppCreateInAppsMenu)); 284 } 285 286 quick_launch_check_box_ = AddCheckbox( 287 (base::win::GetVersion() >= base::win::VERSION_WIN7) ? 288 l10n_util::GetStringUTF16(IDS_PIN_TO_TASKBAR_CHKBOX) : 289 l10n_util::GetStringUTF16( 290 IDS_CREATE_SHORTCUTS_QUICK_LAUNCH_BAR_CHKBOX), 291 profile_->GetPrefs()->GetBoolean(prefs::kWebAppCreateInQuickLaunchBar)); 292 #elif defined(OS_POSIX) 293 menu_check_box_ = AddCheckbox( 294 l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_MENU_CHKBOX), 295 profile_->GetPrefs()->GetBoolean(prefs::kWebAppCreateInAppsMenu)); 296 #endif 297 298 // Layout controls 299 views::GridLayout* layout = views::GridLayout::CreatePanel(this); 300 SetLayoutManager(layout); 301 302 static const int kHeaderColumnSetId = 0; 303 views::ColumnSet* column_set = layout->AddColumnSet(kHeaderColumnSetId); 304 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER, 305 100.0f, views::GridLayout::FIXED, 0, 0); 306 307 static const int kTableColumnSetId = 1; 308 column_set = layout->AddColumnSet(kTableColumnSetId); 309 column_set->AddPaddingColumn(0, views::kPanelHorizIndentation); 310 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 311 100.0f, views::GridLayout::USE_PREF, 0, 0); 312 313 if (app_info_) { 314 layout->StartRow(0, kHeaderColumnSetId); 315 layout->AddView(app_info_); 316 layout->AddPaddingRow(0, views::kPanelSubVerticalSpacing); 317 } 318 319 layout->StartRow(0, kHeaderColumnSetId); 320 layout->AddView(create_shortcuts_label_); 321 322 layout->AddPaddingRow(0, views::kLabelToControlVerticalSpacing); 323 layout->StartRow(0, kTableColumnSetId); 324 layout->AddView(desktop_check_box_); 325 326 if (menu_check_box_ != NULL) { 327 layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing); 328 layout->StartRow(0, kTableColumnSetId); 329 layout->AddView(menu_check_box_); 330 } 331 332 if (quick_launch_check_box_ != NULL) { 333 layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing); 334 layout->StartRow(0, kTableColumnSetId); 335 layout->AddView(quick_launch_check_box_); 336 } 337 } 338 339 gfx::Size CreateApplicationShortcutView::GetPreferredSize() const { 340 // TODO(evanm): should this use IDS_CREATE_SHORTCUTS_DIALOG_WIDTH_CHARS? 341 static const int kDialogWidth = 360; 342 int height = GetLayoutManager()->GetPreferredHeightForWidth(this, 343 kDialogWidth); 344 return gfx::Size(kDialogWidth, height); 345 } 346 347 base::string16 CreateApplicationShortcutView::GetDialogButtonLabel( 348 ui::DialogButton button) const { 349 if (button == ui::DIALOG_BUTTON_OK) 350 return l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_COMMIT); 351 return views::DialogDelegateView::GetDialogButtonLabel(button); 352 } 353 354 bool CreateApplicationShortcutView::IsDialogButtonEnabled( 355 ui::DialogButton button) const { 356 if (button == ui::DIALOG_BUTTON_OK) 357 return desktop_check_box_->checked() || 358 ((menu_check_box_ != NULL) && 359 menu_check_box_->checked()) || 360 ((quick_launch_check_box_ != NULL) && 361 quick_launch_check_box_->checked()); 362 363 return true; 364 } 365 366 ui::ModalType CreateApplicationShortcutView::GetModalType() const { 367 return ui::MODAL_TYPE_WINDOW; 368 } 369 370 base::string16 CreateApplicationShortcutView::GetWindowTitle() const { 371 return l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_TITLE); 372 } 373 374 bool CreateApplicationShortcutView::Accept() { 375 if (!IsDialogButtonEnabled(ui::DIALOG_BUTTON_OK)) 376 return false; 377 378 web_app::ShortcutLocations creation_locations; 379 creation_locations.on_desktop = desktop_check_box_->checked(); 380 if (menu_check_box_ != NULL && menu_check_box_->checked()) { 381 creation_locations.applications_menu_location = 382 create_in_chrome_apps_subdir_ ? 383 web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS : 384 web_app::APP_MENU_LOCATION_ROOT; 385 } 386 387 #if defined(OS_WIN) 388 creation_locations.in_quick_launch_bar = quick_launch_check_box_ == NULL ? 389 NULL : quick_launch_check_box_->checked(); 390 #elif defined(OS_POSIX) 391 // Create shortcut in Mac dock or as Linux (gnome/kde) application launcher 392 // are not implemented yet. 393 creation_locations.in_quick_launch_bar = false; 394 #endif 395 396 web_app::CreateShortcutsForShortcutInfo( 397 web_app::SHORTCUT_CREATION_BY_USER, 398 creation_locations, 399 shortcut_info_); 400 return true; 401 } 402 403 views::Checkbox* CreateApplicationShortcutView::AddCheckbox( 404 const base::string16& text, bool checked) { 405 views::Checkbox* checkbox = new views::Checkbox(text); 406 checkbox->SetChecked(checked); 407 checkbox->set_listener(this); 408 return checkbox; 409 } 410 411 void CreateApplicationShortcutView::ButtonPressed(views::Button* sender, 412 const ui::Event& event) { 413 if (sender == desktop_check_box_) { 414 profile_->GetPrefs()->SetBoolean(prefs::kWebAppCreateOnDesktop, 415 desktop_check_box_->checked()); 416 } else if (sender == menu_check_box_) { 417 profile_->GetPrefs()->SetBoolean(prefs::kWebAppCreateInAppsMenu, 418 menu_check_box_->checked()); 419 } else if (sender == quick_launch_check_box_) { 420 profile_->GetPrefs()->SetBoolean(prefs::kWebAppCreateInQuickLaunchBar, 421 quick_launch_check_box_->checked()); 422 } 423 424 // When no checkbox is checked we should not have the action button enabled. 425 GetDialogClientView()->UpdateDialogButtons(); 426 } 427 428 CreateUrlApplicationShortcutView::CreateUrlApplicationShortcutView( 429 content::WebContents* web_contents) 430 : CreateApplicationShortcutView( 431 Profile::FromBrowserContext(web_contents->GetBrowserContext())), 432 web_contents_(web_contents), 433 pending_download_id_(-1), 434 weak_ptr_factory_(this) { 435 436 web_app::GetShortcutInfoForTab(web_contents_, &shortcut_info_); 437 const WebApplicationInfo& app_info = 438 extensions::TabHelper::FromWebContents(web_contents_)->web_app_info(); 439 if (!app_info.icons.empty()) { 440 web_app::GetIconsInfo(app_info, &unprocessed_icons_); 441 FetchIcon(); 442 } 443 444 // Create URL app shortcuts in the top-level menu. 445 create_in_chrome_apps_subdir_ = false; 446 447 InitControls(DIALOG_LAYOUT_URL_SHORTCUT); 448 } 449 450 CreateUrlApplicationShortcutView::~CreateUrlApplicationShortcutView() { 451 } 452 453 bool CreateUrlApplicationShortcutView::Accept() { 454 if (!CreateApplicationShortcutView::Accept()) 455 return false; 456 457 // Get the smallest icon in the icon family (should have only 1). 458 const gfx::Image* icon = shortcut_info_.favicon.GetBest(0, 0); 459 SkBitmap bitmap = icon ? icon->AsBitmap() : SkBitmap(); 460 extensions::TabHelper::FromWebContents(web_contents_)->SetAppIcon(bitmap); 461 Browser* browser = chrome::FindBrowserWithWebContents(web_contents_); 462 if (browser) 463 chrome::ConvertTabToAppWindow(browser, web_contents_); 464 return true; 465 } 466 467 void CreateUrlApplicationShortcutView::FetchIcon() { 468 // There should only be fetch job at a time. 469 DCHECK_EQ(-1, pending_download_id_); 470 471 if (unprocessed_icons_.empty()) // No icons to fetch. 472 return; 473 474 int preferred_size = std::max(unprocessed_icons_.back().width, 475 unprocessed_icons_.back().height); 476 pending_download_id_ = web_contents_->DownloadImage( 477 unprocessed_icons_.back().url, 478 true, // is a favicon 479 0, // no maximum size 480 base::Bind(&CreateUrlApplicationShortcutView::DidDownloadFavicon, 481 weak_ptr_factory_.GetWeakPtr(), 482 preferred_size)); 483 484 unprocessed_icons_.pop_back(); 485 } 486 487 void CreateUrlApplicationShortcutView::DidDownloadFavicon( 488 int requested_size, 489 int id, 490 int http_status_code, 491 const GURL& image_url, 492 const std::vector<SkBitmap>& bitmaps, 493 const std::vector<gfx::Size>& original_bitmap_sizes) { 494 if (id != pending_download_id_) 495 return; 496 pending_download_id_ = -1; 497 498 gfx::ImageSkia image_skia = CreateFaviconImageSkia( 499 bitmaps, 500 original_bitmap_sizes, 501 requested_size, 502 NULL); 503 if (!image_skia.isNull()) { 504 shortcut_info_.favicon.Add(image_skia); 505 static_cast<AppInfoView*>(app_info_)->UpdateIcon(shortcut_info_.favicon); 506 } else { 507 FetchIcon(); 508 } 509 } 510 511 CreateChromeApplicationShortcutView::CreateChromeApplicationShortcutView( 512 Profile* profile, 513 const extensions::Extension* app, 514 const base::Callback<void(bool)>& close_callback) 515 : CreateApplicationShortcutView(profile), 516 close_callback_(close_callback), 517 weak_ptr_factory_(this) { 518 // Place Chrome app shortcuts in the "Chrome Apps" submenu. 519 create_in_chrome_apps_subdir_ = true; 520 521 InitControls(DIALOG_LAYOUT_APP_SHORTCUT); 522 523 // Get shortcut information and icon; they are needed for creating the 524 // shortcut. 525 web_app::UpdateShortcutInfoAndIconForApp( 526 app, 527 profile, 528 base::Bind(&CreateChromeApplicationShortcutView::OnShortcutInfoLoaded, 529 weak_ptr_factory_.GetWeakPtr())); 530 } 531 532 CreateChromeApplicationShortcutView::~CreateChromeApplicationShortcutView() {} 533 534 bool CreateChromeApplicationShortcutView::Accept() { 535 if (!close_callback_.is_null()) 536 close_callback_.Run(true); 537 return CreateApplicationShortcutView::Accept(); 538 } 539 540 bool CreateChromeApplicationShortcutView::Cancel() { 541 if (!close_callback_.is_null()) 542 close_callback_.Run(false); 543 return CreateApplicationShortcutView::Cancel(); 544 } 545 546 // Called when the app's ShortcutInfo (with icon) is loaded. 547 void CreateChromeApplicationShortcutView::OnShortcutInfoLoaded( 548 const web_app::ShortcutInfo& shortcut_info) { 549 shortcut_info_ = shortcut_info; 550 } 551