1 // Copyright (c) 2011 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/gtk/download/download_item_gtk.h" 6 7 #include "base/basictypes.h" 8 #include "base/callback.h" 9 #include "base/metrics/histogram.h" 10 #include "base/string_util.h" 11 #include "base/time.h" 12 #include "base/utf_string_conversions.h" 13 #include "chrome/browser/browser_process.h" 14 #include "chrome/browser/download/download_item.h" 15 #include "chrome/browser/download/download_item_model.h" 16 #include "chrome/browser/download/download_manager.h" 17 #include "chrome/browser/download/download_shelf.h" 18 #include "chrome/browser/download/download_util.h" 19 #include "chrome/browser/ui/browser.h" 20 #include "chrome/browser/ui/gtk/custom_drag.h" 21 #include "chrome/browser/ui/gtk/download/download_shelf_gtk.h" 22 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 23 #include "chrome/browser/ui/gtk/gtk_util.h" 24 #include "chrome/browser/ui/gtk/menu_gtk.h" 25 #include "chrome/browser/ui/gtk/nine_box.h" 26 #include "content/common/notification_service.h" 27 #include "grit/generated_resources.h" 28 #include "grit/theme_resources.h" 29 #include "third_party/skia/include/core/SkBitmap.h" 30 #include "ui/base/animation/slide_animation.h" 31 #include "ui/base/l10n/l10n_util.h" 32 #include "ui/base/resource/resource_bundle.h" 33 #include "ui/base/text/text_elider.h" 34 #include "ui/gfx/canvas_skia_paint.h" 35 #include "ui/gfx/color_utils.h" 36 #include "ui/gfx/font.h" 37 #include "ui/gfx/image.h" 38 #include "ui/gfx/skia_utils_gtk.h" 39 40 namespace { 41 42 // The width of the |menu_button_| widget. It has to be at least as wide as the 43 // bitmap that we use to draw it, i.e. 16, but can be more. 44 const int kMenuButtonWidth = 16; 45 46 // Padding on left and right of items in dangerous download prompt. 47 const int kDangerousElementPadding = 3; 48 49 // Amount of space we allot to showing the filename. If the filename is too wide 50 // it will be elided. 51 const int kTextWidth = 140; 52 53 // We only cap the size of the tooltip so we don't crash. 54 const int kTooltipMaxWidth = 1000; 55 56 // The minimum width we will ever draw the download item. Used as a lower bound 57 // during animation. This number comes from the width of the images used to 58 // make the download item. 59 const int kMinDownloadItemWidth = download_util::kSmallProgressIconSize; 60 61 // New download item animation speed in milliseconds. 62 const int kNewItemAnimationDurationMs = 800; 63 64 // How long the 'download complete/interrupted' animation should last for. 65 const int kCompleteAnimationDurationMs = 2500; 66 67 // Width of the body area of the download item. 68 // TODO(estade): get rid of the fudge factor. http://crbug.com/18692 69 const int kBodyWidth = kTextWidth + 50 + download_util::kSmallProgressIconSize; 70 71 // The font size of the text, and that size rounded down to the nearest integer 72 // for the size of the arrow in GTK theme mode. 73 const double kTextSize = 13.4; // 13.4px == 10pt @ 96dpi 74 75 // Darken light-on-dark download status text by 20% before drawing, thus 76 // creating a "muted" version of title text for both dark-on-light and 77 // light-on-dark themes. 78 static const double kDownloadItemLuminanceMod = 0.8; 79 80 } // namespace 81 82 // DownloadShelfContextMenuGtk ------------------------------------------------- 83 84 class DownloadShelfContextMenuGtk : public DownloadShelfContextMenu, 85 public MenuGtk::Delegate { 86 public: 87 // The constructor creates the menu and immediately pops it up. 88 // |model| is the download item model associated with this context menu, 89 // |widget| is the button that popped up this context menu, and |e| is 90 // the button press event that caused this menu to be created. 91 DownloadShelfContextMenuGtk(BaseDownloadItemModel* model, 92 DownloadItemGtk* download_item) 93 : DownloadShelfContextMenu(model), 94 download_item_(download_item) { 95 } 96 97 ~DownloadShelfContextMenuGtk() { 98 } 99 100 void Popup(GtkWidget* widget, GdkEventButton* event) { 101 // Create the menu if we have not created it yet or we created it for 102 // an in-progress download that has since completed. 103 if (download_->IsComplete()) 104 menu_.reset(new MenuGtk(this, GetFinishedMenuModel())); 105 else 106 menu_.reset(new MenuGtk(this, GetInProgressMenuModel())); 107 108 if (widget) 109 menu_->PopupForWidget(widget, event->button, event->time); 110 else 111 menu_->PopupAsContext(gfx::Point(event->x_root, event->y_root), 112 event->time); 113 } 114 115 // MenuGtk::Delegate implementation: 116 virtual void StoppedShowing() { 117 download_item_->menu_showing_ = false; 118 gtk_widget_queue_draw(download_item_->menu_button_); 119 } 120 121 virtual GtkWidget* GetImageForCommandId(int command_id) const { 122 const char* stock = NULL; 123 switch (command_id) { 124 case SHOW_IN_FOLDER: 125 case OPEN_WHEN_COMPLETE: 126 stock = GTK_STOCK_OPEN; 127 break; 128 129 case CANCEL: 130 stock = GTK_STOCK_CANCEL; 131 break; 132 133 case ALWAYS_OPEN_TYPE: 134 case TOGGLE_PAUSE: 135 stock = NULL; 136 } 137 138 return stock ? gtk_image_new_from_stock(stock, GTK_ICON_SIZE_MENU) : NULL; 139 } 140 141 private: 142 // The menu we show on Popup(). We keep a pointer to it for a couple reasons: 143 // * we don't want to have to recreate the menu every time it's popped up. 144 // * we have to keep it in scope for longer than the duration of Popup(), or 145 // completing the user-selected action races against the menu's 146 // destruction. 147 scoped_ptr<MenuGtk> menu_; 148 149 // The download item that created us. 150 DownloadItemGtk* download_item_; 151 }; 152 153 // DownloadItemGtk ------------------------------------------------------------- 154 155 NineBox* DownloadItemGtk::body_nine_box_normal_ = NULL; 156 NineBox* DownloadItemGtk::body_nine_box_prelight_ = NULL; 157 NineBox* DownloadItemGtk::body_nine_box_active_ = NULL; 158 159 NineBox* DownloadItemGtk::menu_nine_box_normal_ = NULL; 160 NineBox* DownloadItemGtk::menu_nine_box_prelight_ = NULL; 161 NineBox* DownloadItemGtk::menu_nine_box_active_ = NULL; 162 163 NineBox* DownloadItemGtk::dangerous_nine_box_ = NULL; 164 165 DownloadItemGtk::DownloadItemGtk(DownloadShelfGtk* parent_shelf, 166 BaseDownloadItemModel* download_model) 167 : parent_shelf_(parent_shelf), 168 arrow_(NULL), 169 menu_showing_(false), 170 theme_service_(GtkThemeService::GetFrom( 171 parent_shelf->browser()->profile())), 172 progress_angle_(download_util::kStartAngleDegrees), 173 download_model_(download_model), 174 dangerous_prompt_(NULL), 175 dangerous_label_(NULL), 176 complete_animation_(this), 177 icon_small_(NULL), 178 icon_large_(NULL), 179 creation_time_(base::Time::Now()), 180 download_complete_(false) { 181 LoadIcon(); 182 183 body_.Own(gtk_button_new()); 184 gtk_widget_set_app_paintable(body_.get(), TRUE); 185 UpdateTooltip(); 186 187 g_signal_connect(body_.get(), "expose-event", 188 G_CALLBACK(OnExposeThunk), this); 189 g_signal_connect(body_.get(), "clicked", 190 G_CALLBACK(OnClickThunk), this); 191 g_signal_connect(body_.get(), "button-press-event", 192 G_CALLBACK(OnButtonPressThunk), this); 193 GTK_WIDGET_UNSET_FLAGS(body_.get(), GTK_CAN_FOCUS); 194 // Remove internal padding on the button. 195 GtkRcStyle* no_padding_style = gtk_rc_style_new(); 196 no_padding_style->xthickness = 0; 197 no_padding_style->ythickness = 0; 198 gtk_widget_modify_style(body_.get(), no_padding_style); 199 g_object_unref(no_padding_style); 200 201 name_label_ = gtk_label_new(NULL); 202 203 UpdateNameLabel(); 204 205 status_label_ = gtk_label_new(NULL); 206 g_signal_connect(status_label_, "destroy", 207 G_CALLBACK(gtk_widget_destroyed), &status_label_); 208 // Left align and vertically center the labels. 209 gtk_misc_set_alignment(GTK_MISC(name_label_), 0, 0.5); 210 gtk_misc_set_alignment(GTK_MISC(status_label_), 0, 0.5); 211 // Until we switch to vector graphics, force the font size. 212 gtk_util::ForceFontSizePixels(name_label_, kTextSize); 213 gtk_util::ForceFontSizePixels(status_label_, kTextSize); 214 215 // Stack the labels on top of one another. 216 GtkWidget* text_stack = gtk_vbox_new(FALSE, 0); 217 gtk_box_pack_start(GTK_BOX(text_stack), name_label_, TRUE, TRUE, 0); 218 gtk_box_pack_start(GTK_BOX(text_stack), status_label_, FALSE, FALSE, 0); 219 220 // We use a GtkFixed because we don't want it to have its own window. 221 // This choice of widget is not critically important though. 222 progress_area_.Own(gtk_fixed_new()); 223 gtk_widget_set_size_request(progress_area_.get(), 224 download_util::kSmallProgressIconSize, 225 download_util::kSmallProgressIconSize); 226 gtk_widget_set_app_paintable(progress_area_.get(), TRUE); 227 g_signal_connect(progress_area_.get(), "expose-event", 228 G_CALLBACK(OnProgressAreaExposeThunk), this); 229 230 // Put the download progress icon on the left of the labels. 231 GtkWidget* body_hbox = gtk_hbox_new(FALSE, 0); 232 gtk_container_add(GTK_CONTAINER(body_.get()), body_hbox); 233 gtk_box_pack_start(GTK_BOX(body_hbox), progress_area_.get(), FALSE, FALSE, 0); 234 gtk_box_pack_start(GTK_BOX(body_hbox), text_stack, TRUE, TRUE, 0); 235 236 menu_button_ = gtk_button_new(); 237 gtk_widget_set_app_paintable(menu_button_, TRUE); 238 GTK_WIDGET_UNSET_FLAGS(menu_button_, GTK_CAN_FOCUS); 239 g_signal_connect(menu_button_, "expose-event", 240 G_CALLBACK(OnExposeThunk), this); 241 g_signal_connect(menu_button_, "button-press-event", 242 G_CALLBACK(OnMenuButtonPressEventThunk), this); 243 g_object_set_data(G_OBJECT(menu_button_), "left-align-popup", 244 reinterpret_cast<void*>(true)); 245 246 GtkWidget* shelf_hbox = parent_shelf->GetHBox(); 247 hbox_.Own(gtk_hbox_new(FALSE, 0)); 248 g_signal_connect(hbox_.get(), "expose-event", 249 G_CALLBACK(OnHboxExposeThunk), this); 250 gtk_box_pack_start(GTK_BOX(hbox_.get()), body_.get(), FALSE, FALSE, 0); 251 gtk_box_pack_start(GTK_BOX(hbox_.get()), menu_button_, FALSE, FALSE, 0); 252 gtk_box_pack_start(GTK_BOX(shelf_hbox), hbox_.get(), FALSE, FALSE, 0); 253 // Insert as the leftmost item. 254 gtk_box_reorder_child(GTK_BOX(shelf_hbox), hbox_.get(), 0); 255 256 get_download()->AddObserver(this); 257 258 new_item_animation_.reset(new ui::SlideAnimation(this)); 259 new_item_animation_->SetSlideDuration(kNewItemAnimationDurationMs); 260 gtk_widget_show_all(hbox_.get()); 261 262 if (IsDangerous()) { 263 // Hide the download item components for now. 264 gtk_widget_hide(body_.get()); 265 gtk_widget_hide(menu_button_); 266 267 // Create an hbox to hold it all. 268 dangerous_hbox_.Own(gtk_hbox_new(FALSE, kDangerousElementPadding)); 269 270 // Add padding at the beginning and end. The hbox will add padding between 271 // the empty labels and the other elements. 272 GtkWidget* empty_label_a = gtk_label_new(NULL); 273 GtkWidget* empty_label_b = gtk_label_new(NULL); 274 gtk_box_pack_start(GTK_BOX(dangerous_hbox_.get()), empty_label_a, 275 FALSE, FALSE, 0); 276 gtk_box_pack_end(GTK_BOX(dangerous_hbox_.get()), empty_label_b, 277 FALSE, FALSE, 0); 278 279 // Create the warning icon. 280 dangerous_image_ = gtk_image_new(); 281 gtk_box_pack_start(GTK_BOX(dangerous_hbox_.get()), dangerous_image_, 282 FALSE, FALSE, 0); 283 284 dangerous_label_ = gtk_label_new(NULL); 285 // We pass TRUE, TRUE so that the label will condense to less than its 286 // request when the animation is going on. 287 gtk_box_pack_start(GTK_BOX(dangerous_hbox_.get()), dangerous_label_, 288 TRUE, TRUE, 0); 289 290 // Create the nevermind button. 291 GtkWidget* dangerous_decline = gtk_button_new_with_label( 292 l10n_util::GetStringUTF8(IDS_DISCARD_DOWNLOAD).c_str()); 293 g_signal_connect(dangerous_decline, "clicked", 294 G_CALLBACK(OnDangerousDeclineThunk), this); 295 gtk_util::CenterWidgetInHBox(dangerous_hbox_.get(), dangerous_decline, 296 false, 0); 297 298 // Create the ok button. 299 GtkWidget* dangerous_accept = gtk_button_new_with_label( 300 l10n_util::GetStringUTF8( 301 download_model->download()->is_extension_install() ? 302 IDS_CONTINUE_EXTENSION_DOWNLOAD : IDS_SAVE_DOWNLOAD).c_str()); 303 g_signal_connect(dangerous_accept, "clicked", 304 G_CALLBACK(OnDangerousAcceptThunk), this); 305 gtk_util::CenterWidgetInHBox(dangerous_hbox_.get(), dangerous_accept, false, 306 0); 307 308 // Put it in an alignment so that padding will be added on the left and 309 // right. 310 dangerous_prompt_ = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); 311 gtk_alignment_set_padding(GTK_ALIGNMENT(dangerous_prompt_), 312 0, 0, kDangerousElementPadding, kDangerousElementPadding); 313 gtk_container_add(GTK_CONTAINER(dangerous_prompt_), dangerous_hbox_.get()); 314 gtk_box_pack_start(GTK_BOX(hbox_.get()), dangerous_prompt_, FALSE, FALSE, 315 0); 316 gtk_widget_set_app_paintable(dangerous_prompt_, TRUE); 317 gtk_widget_set_redraw_on_allocate(dangerous_prompt_, TRUE); 318 g_signal_connect(dangerous_prompt_, "expose-event", 319 G_CALLBACK(OnDangerousPromptExposeThunk), this); 320 gtk_widget_show_all(dangerous_prompt_); 321 } 322 323 registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, 324 NotificationService::AllSources()); 325 theme_service_->InitThemesFor(this); 326 327 // Set the initial width of the widget to be animated. 328 if (IsDangerous()) { 329 gtk_widget_set_size_request(dangerous_hbox_.get(), 330 dangerous_hbox_start_width_, -1); 331 } else { 332 gtk_widget_set_size_request(body_.get(), kMinDownloadItemWidth, -1); 333 } 334 335 new_item_animation_->Show(); 336 337 complete_animation_.SetTweenType(ui::Tween::LINEAR); 338 complete_animation_.SetSlideDuration(kCompleteAnimationDurationMs); 339 } 340 341 DownloadItemGtk::~DownloadItemGtk() { 342 icon_consumer_.CancelAllRequests(); 343 StopDownloadProgress(); 344 get_download()->RemoveObserver(this); 345 346 // We may free some shelf space for showing more download items. 347 parent_shelf_->MaybeShowMoreDownloadItems(); 348 349 hbox_.Destroy(); 350 progress_area_.Destroy(); 351 body_.Destroy(); 352 dangerous_hbox_.Destroy(); 353 354 // Make sure this widget has been destroyed and the pointer we hold to it 355 // NULLed. 356 DCHECK(!status_label_); 357 } 358 359 void DownloadItemGtk::OnDownloadUpdated(DownloadItem* download) { 360 DCHECK_EQ(download, get_download()); 361 362 if (dangerous_prompt_ != NULL && 363 download->safety_state() == DownloadItem::DANGEROUS_BUT_VALIDATED) { 364 // We have been approved. 365 gtk_widget_show_all(hbox_.get()); 366 gtk_widget_destroy(dangerous_prompt_); 367 gtk_widget_set_size_request(body_.get(), kBodyWidth, -1); 368 dangerous_prompt_ = NULL; 369 370 // We may free some shelf space for showing more download items. 371 parent_shelf_->MaybeShowMoreDownloadItems(); 372 } 373 374 if (download->GetUserVerifiedFilePath() != icon_filepath_) { 375 // Turns out the file path is "Unconfirmed %d.crdownload" for dangerous 376 // downloads. When the download is confirmed, the file is renamed on 377 // another thread, so reload the icon if the download filename changes. 378 LoadIcon(); 379 380 UpdateTooltip(); 381 } 382 383 switch (download->state()) { 384 case DownloadItem::REMOVING: 385 parent_shelf_->RemoveDownloadItem(this); // This will delete us! 386 return; 387 case DownloadItem::CANCELLED: 388 StopDownloadProgress(); 389 gtk_widget_queue_draw(progress_area_.get()); 390 break; 391 case DownloadItem::INTERRUPTED: 392 StopDownloadProgress(); 393 394 complete_animation_.Show(); 395 break; 396 case DownloadItem::COMPLETE: 397 if (download_complete_) 398 // We've already handled the completion specific actions; skip 399 // doing them again. 400 break; 401 402 if (download->auto_opened()) { 403 parent_shelf_->RemoveDownloadItem(this); // This will delete us! 404 return; 405 } 406 StopDownloadProgress(); 407 408 // Set up the widget as a drag source. 409 DownloadItemDrag::SetSource(body_.get(), get_download(), icon_large_); 410 411 complete_animation_.Show(); 412 download_complete_ = true; 413 break; 414 case DownloadItem::IN_PROGRESS: 415 get_download()->is_paused() ? 416 StopDownloadProgress() : StartDownloadProgress(); 417 break; 418 default: 419 NOTREACHED(); 420 } 421 422 // Now update the status label. We may have already removed it; if so, we 423 // do nothing. 424 if (!status_label_) { 425 return; 426 } 427 428 status_text_ = UTF16ToUTF8(download_model_->GetStatusText()); 429 // Remove the status text label. 430 if (status_text_.empty()) { 431 gtk_widget_destroy(status_label_); 432 return; 433 } 434 435 UpdateStatusLabel(status_text_); 436 } 437 438 void DownloadItemGtk::AnimationProgressed(const ui::Animation* animation) { 439 if (animation == &complete_animation_) { 440 gtk_widget_queue_draw(progress_area_.get()); 441 } else { 442 DCHECK(animation == new_item_animation_.get()); 443 if (IsDangerous()) { 444 int progress = static_cast<int>((dangerous_hbox_full_width_ - 445 dangerous_hbox_start_width_) * 446 animation->GetCurrentValue()); 447 int showing_width = dangerous_hbox_start_width_ + progress; 448 gtk_widget_set_size_request(dangerous_hbox_.get(), showing_width, -1); 449 } else { 450 int showing_width = std::max(kMinDownloadItemWidth, 451 static_cast<int>(kBodyWidth * animation->GetCurrentValue())); 452 gtk_widget_set_size_request(body_.get(), showing_width, -1); 453 } 454 } 455 } 456 457 void DownloadItemGtk::Observe(NotificationType type, 458 const NotificationSource& source, 459 const NotificationDetails& details) { 460 if (type == NotificationType::BROWSER_THEME_CHANGED) { 461 // Our GtkArrow is only visible in gtk mode. Otherwise, we let the custom 462 // rendering code do whatever it wants. 463 if (theme_service_->UseGtkTheme()) { 464 if (!arrow_) { 465 arrow_ = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE); 466 gtk_widget_set_size_request(arrow_, 467 static_cast<int>(kTextSize), 468 static_cast<int>(kTextSize)); 469 gtk_container_add(GTK_CONTAINER(menu_button_), arrow_); 470 } 471 472 gtk_widget_set_size_request(menu_button_, -1, -1); 473 gtk_widget_show(arrow_); 474 } else { 475 InitNineBoxes(); 476 477 gtk_widget_set_size_request(menu_button_, kMenuButtonWidth, 0); 478 479 if (arrow_) 480 gtk_widget_hide(arrow_); 481 } 482 483 UpdateNameLabel(); 484 UpdateStatusLabel(status_text_); 485 UpdateDangerWarning(); 486 } 487 } 488 489 DownloadItem* DownloadItemGtk::get_download() { 490 return download_model_->download(); 491 } 492 493 bool DownloadItemGtk::IsDangerous() { 494 return get_download()->safety_state() == DownloadItem::DANGEROUS; 495 } 496 497 // Download progress animation functions. 498 499 void DownloadItemGtk::UpdateDownloadProgress() { 500 progress_angle_ = (progress_angle_ + 501 download_util::kUnknownIncrementDegrees) % 502 download_util::kMaxDegrees; 503 gtk_widget_queue_draw(progress_area_.get()); 504 } 505 506 void DownloadItemGtk::StartDownloadProgress() { 507 if (progress_timer_.IsRunning()) 508 return; 509 progress_timer_.Start( 510 base::TimeDelta::FromMilliseconds(download_util::kProgressRateMs), this, 511 &DownloadItemGtk::UpdateDownloadProgress); 512 } 513 514 void DownloadItemGtk::StopDownloadProgress() { 515 progress_timer_.Stop(); 516 } 517 518 // Icon loading functions. 519 520 void DownloadItemGtk::OnLoadSmallIconComplete(IconManager::Handle handle, 521 gfx::Image* image) { 522 icon_small_ = image; 523 gtk_widget_queue_draw(progress_area_.get()); 524 } 525 526 void DownloadItemGtk::OnLoadLargeIconComplete(IconManager::Handle handle, 527 gfx::Image* image) { 528 icon_large_ = image; 529 DownloadItemDrag::SetSource(body_.get(), get_download(), icon_large_); 530 } 531 532 void DownloadItemGtk::LoadIcon() { 533 icon_consumer_.CancelAllRequests(); 534 IconManager* im = g_browser_process->icon_manager(); 535 icon_filepath_ = get_download()->GetUserVerifiedFilePath(); 536 im->LoadIcon(icon_filepath_, 537 IconLoader::SMALL, &icon_consumer_, 538 NewCallback(this, &DownloadItemGtk::OnLoadSmallIconComplete)); 539 im->LoadIcon(icon_filepath_, 540 IconLoader::LARGE, &icon_consumer_, 541 NewCallback(this, &DownloadItemGtk::OnLoadLargeIconComplete)); 542 } 543 544 void DownloadItemGtk::UpdateTooltip() { 545 string16 elided_filename = ui::ElideFilename( 546 get_download()->GetFileNameToReportUser(), 547 gfx::Font(), kTooltipMaxWidth); 548 gtk_widget_set_tooltip_text(body_.get(), 549 UTF16ToUTF8(elided_filename).c_str()); 550 } 551 552 void DownloadItemGtk::UpdateNameLabel() { 553 // TODO(estade): This is at best an educated guess, since we don't actually 554 // use gfx::Font() to draw the text. This is why we need to add so 555 // much padding when we set the size request. We need to either use gfx::Font 556 // or somehow extend TextElider. 557 string16 elided_filename = ui::ElideFilename( 558 get_download()->GetFileNameToReportUser(), 559 gfx::Font(), kTextWidth); 560 561 GdkColor color = theme_service_->GetGdkColor( 562 ThemeService::COLOR_BOOKMARK_TEXT); 563 gtk_util::SetLabelColor(name_label_, theme_service_->UseGtkTheme() ? 564 NULL : &color); 565 gtk_label_set_text(GTK_LABEL(name_label_), 566 UTF16ToUTF8(elided_filename).c_str()); 567 } 568 569 void DownloadItemGtk::UpdateStatusLabel(const std::string& status_text) { 570 if (!status_label_) 571 return; 572 573 GdkColor text_color; 574 if (!theme_service_->UseGtkTheme()) { 575 SkColor color = theme_service_->GetColor( 576 ThemeService::COLOR_BOOKMARK_TEXT); 577 if (color_utils::RelativeLuminance(color) > 0.5) { 578 color = SkColorSetRGB( 579 static_cast<int>(kDownloadItemLuminanceMod * 580 SkColorGetR(color)), 581 static_cast<int>(kDownloadItemLuminanceMod * 582 SkColorGetG(color)), 583 static_cast<int>(kDownloadItemLuminanceMod * 584 SkColorGetB(color))); 585 } 586 587 // Lighten the color by blending it with the download item body color. These 588 // values are taken from IDR_DOWNLOAD_BUTTON. 589 SkColor blend_color = SkColorSetRGB(241, 245, 250); 590 text_color = gfx::SkColorToGdkColor( 591 color_utils::AlphaBlend(blend_color, color, 77)); 592 } 593 594 gtk_util::SetLabelColor(status_label_, theme_service_->UseGtkTheme() ? 595 NULL : &text_color); 596 gtk_label_set_text(GTK_LABEL(status_label_), status_text.c_str()); 597 } 598 599 void DownloadItemGtk::UpdateDangerWarning() { 600 if (dangerous_prompt_) { 601 UpdateDangerIcon(); 602 603 // We create |dangerous_warning| as a wide string so we can more easily 604 // calculate its length in characters. 605 string16 dangerous_warning; 606 607 // The dangerous download label text is different for different cases. 608 if (get_download()->danger_type() == DownloadItem::DANGEROUS_URL) { 609 // Safebrowsing shows the download URL leads to malicious file. 610 dangerous_warning = 611 l10n_util::GetStringUTF16(IDS_PROMPT_UNSAFE_DOWNLOAD_URL); 612 } else { 613 // It's a dangerous file type (e.g.: an executable). 614 DCHECK(get_download()->danger_type() == DownloadItem::DANGEROUS_FILE); 615 if (get_download()->is_extension_install()) { 616 dangerous_warning = 617 l10n_util::GetStringUTF16(IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION); 618 } else { 619 string16 elided_filename = ui::ElideFilename( 620 get_download()->target_name(), gfx::Font(), kTextWidth); 621 dangerous_warning = 622 l10n_util::GetStringFUTF16(IDS_PROMPT_DANGEROUS_DOWNLOAD, 623 elided_filename); 624 } 625 } 626 627 if (theme_service_->UseGtkTheme()) { 628 gtk_util::SetLabelColor(dangerous_label_, NULL); 629 } else { 630 GdkColor color = theme_service_->GetGdkColor( 631 ThemeService::COLOR_BOOKMARK_TEXT); 632 gtk_util::SetLabelColor(dangerous_label_, &color); 633 } 634 635 gtk_label_set_text(GTK_LABEL(dangerous_label_), 636 UTF16ToUTF8(dangerous_warning).c_str()); 637 638 // Until we switch to vector graphics, force the font size. 639 gtk_util::ForceFontSizePixels(dangerous_label_, kTextSize); 640 641 // Get the label width when displaying in one line, and reduce it to 60% to 642 // wrap the label into two lines. 643 gtk_widget_set_size_request(dangerous_label_, -1, -1); 644 gtk_label_set_line_wrap(GTK_LABEL(dangerous_label_), FALSE); 645 646 GtkRequisition req; 647 gtk_widget_size_request(dangerous_label_, &req); 648 649 gint label_width = req.width * 6 / 10; 650 gtk_label_set_line_wrap(GTK_LABEL(dangerous_label_), TRUE); 651 gtk_widget_set_size_request(dangerous_label_, label_width, -1); 652 653 // The width will depend on the text. We must do this each time we possibly 654 // change the label above. 655 gtk_widget_size_request(dangerous_hbox_.get(), &req); 656 dangerous_hbox_full_width_ = req.width; 657 dangerous_hbox_start_width_ = dangerous_hbox_full_width_ - label_width; 658 } 659 } 660 661 void DownloadItemGtk::UpdateDangerIcon() { 662 if (theme_service_->UseGtkTheme()) { 663 const char* stock = 664 get_download()->danger_type() == DownloadItem::DANGEROUS_URL ? 665 GTK_STOCK_DIALOG_ERROR : GTK_STOCK_DIALOG_WARNING; 666 gtk_image_set_from_stock( 667 GTK_IMAGE(dangerous_image_), stock, GTK_ICON_SIZE_SMALL_TOOLBAR); 668 } else { 669 // Set the warning icon. 670 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 671 int pixbuf_id = 672 get_download()->danger_type() == DownloadItem::DANGEROUS_URL ? 673 IDR_SAFEBROWSING_WARNING : IDR_WARNING; 674 GdkPixbuf* download_pixbuf = rb.GetPixbufNamed(pixbuf_id); 675 gtk_image_set_from_pixbuf(GTK_IMAGE(dangerous_image_), download_pixbuf); 676 } 677 } 678 679 // static 680 void DownloadItemGtk::InitNineBoxes() { 681 if (body_nine_box_normal_) 682 return; 683 684 body_nine_box_normal_ = new NineBox( 685 IDR_DOWNLOAD_BUTTON_LEFT_TOP, 686 IDR_DOWNLOAD_BUTTON_CENTER_TOP, 687 IDR_DOWNLOAD_BUTTON_RIGHT_TOP, 688 IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE, 689 IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE, 690 IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE, 691 IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM, 692 IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM, 693 IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM); 694 695 body_nine_box_prelight_ = new NineBox( 696 IDR_DOWNLOAD_BUTTON_LEFT_TOP_H, 697 IDR_DOWNLOAD_BUTTON_CENTER_TOP_H, 698 IDR_DOWNLOAD_BUTTON_RIGHT_TOP_H, 699 IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_H, 700 IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_H, 701 IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_H, 702 IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_H, 703 IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_H, 704 IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_H); 705 706 body_nine_box_active_ = new NineBox( 707 IDR_DOWNLOAD_BUTTON_LEFT_TOP_P, 708 IDR_DOWNLOAD_BUTTON_CENTER_TOP_P, 709 IDR_DOWNLOAD_BUTTON_RIGHT_TOP_P, 710 IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_P, 711 IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_P, 712 IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_P, 713 IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_P, 714 IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_P, 715 IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_P); 716 717 menu_nine_box_normal_ = new NineBox( 718 IDR_DOWNLOAD_BUTTON_MENU_TOP, 0, 0, 719 IDR_DOWNLOAD_BUTTON_MENU_MIDDLE, 0, 0, 720 IDR_DOWNLOAD_BUTTON_MENU_BOTTOM, 0, 0); 721 722 menu_nine_box_prelight_ = new NineBox( 723 IDR_DOWNLOAD_BUTTON_MENU_TOP_H, 0, 0, 724 IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_H, 0, 0, 725 IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_H, 0, 0); 726 727 menu_nine_box_active_ = new NineBox( 728 IDR_DOWNLOAD_BUTTON_MENU_TOP_P, 0, 0, 729 IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_P, 0, 0, 730 IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_P, 0, 0); 731 732 dangerous_nine_box_ = new NineBox( 733 IDR_DOWNLOAD_BUTTON_LEFT_TOP, 734 IDR_DOWNLOAD_BUTTON_CENTER_TOP, 735 IDR_DOWNLOAD_BUTTON_RIGHT_TOP_NO_DD, 736 IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE, 737 IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE, 738 IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_NO_DD, 739 IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM, 740 IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM, 741 IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_NO_DD); 742 } 743 744 gboolean DownloadItemGtk::OnHboxExpose(GtkWidget* widget, GdkEventExpose* e) { 745 if (theme_service_->UseGtkTheme()) { 746 int border_width = GTK_CONTAINER(widget)->border_width; 747 int x = widget->allocation.x + border_width; 748 int y = widget->allocation.y + border_width; 749 int width = widget->allocation.width - border_width * 2; 750 int height = widget->allocation.height - border_width * 2; 751 752 if (IsDangerous()) { 753 // Draw a simple frame around the area when we're displaying the warning. 754 gtk_paint_shadow(widget->style, widget->window, 755 static_cast<GtkStateType>(widget->state), 756 static_cast<GtkShadowType>(GTK_SHADOW_OUT), 757 &e->area, widget, "frame", 758 x, y, width, height); 759 } else { 760 // Manually draw the GTK button border around the download item. We draw 761 // the left part of the button (the file), a divider, and then the right 762 // part of the button (the menu). We can't draw a button on top of each 763 // other (*cough*Clearlooks*cough*) so instead, to draw the left part of 764 // the button, we instruct GTK to draw the entire button...with a 765 // doctored clip rectangle to the left part of the button sans 766 // separator. We then repeat this for the right button. 767 GtkStyle* style = body_.get()->style; 768 769 GtkAllocation left_allocation = body_.get()->allocation; 770 GdkRectangle left_clip = { 771 left_allocation.x, left_allocation.y, 772 left_allocation.width, left_allocation.height 773 }; 774 775 GtkAllocation right_allocation = menu_button_->allocation; 776 GdkRectangle right_clip = { 777 right_allocation.x, right_allocation.y, 778 right_allocation.width, right_allocation.height 779 }; 780 781 GtkShadowType body_shadow = 782 GTK_BUTTON(body_.get())->depressed ? GTK_SHADOW_IN : GTK_SHADOW_OUT; 783 gtk_paint_box(style, widget->window, 784 static_cast<GtkStateType>(GTK_WIDGET_STATE(body_.get())), 785 body_shadow, 786 &left_clip, widget, "button", 787 x, y, width, height); 788 789 GtkShadowType menu_shadow = 790 GTK_BUTTON(menu_button_)->depressed ? GTK_SHADOW_IN : GTK_SHADOW_OUT; 791 gtk_paint_box(style, widget->window, 792 static_cast<GtkStateType>(GTK_WIDGET_STATE(menu_button_)), 793 menu_shadow, 794 &right_clip, widget, "button", 795 x, y, width, height); 796 797 // Doing the math to reverse engineer where we should be drawing our line 798 // is hard and relies on copying GTK internals, so instead steal the 799 // allocation of the gtk arrow which is close enough (and will error on 800 // the conservative side). 801 GtkAllocation arrow_allocation = arrow_->allocation; 802 gtk_paint_vline(style, widget->window, 803 static_cast<GtkStateType>(GTK_WIDGET_STATE(widget)), 804 &e->area, widget, "button", 805 arrow_allocation.y, 806 arrow_allocation.y + arrow_allocation.height, 807 left_allocation.x + left_allocation.width); 808 } 809 } 810 return FALSE; 811 } 812 813 gboolean DownloadItemGtk::OnExpose(GtkWidget* widget, GdkEventExpose* e) { 814 if (!theme_service_->UseGtkTheme()) { 815 bool is_body = widget == body_.get(); 816 817 NineBox* nine_box = NULL; 818 // If true, this widget is |body_|, otherwise it is |menu_button_|. 819 if (GTK_WIDGET_STATE(widget) == GTK_STATE_PRELIGHT) 820 nine_box = is_body ? body_nine_box_prelight_ : menu_nine_box_prelight_; 821 else if (GTK_WIDGET_STATE(widget) == GTK_STATE_ACTIVE) 822 nine_box = is_body ? body_nine_box_active_ : menu_nine_box_active_; 823 else 824 nine_box = is_body ? body_nine_box_normal_ : menu_nine_box_normal_; 825 826 // When the button is showing, we want to draw it as active. We have to do 827 // this explicitly because the button's state will be NORMAL while the menu 828 // has focus. 829 if (!is_body && menu_showing_) 830 nine_box = menu_nine_box_active_; 831 832 nine_box->RenderToWidget(widget); 833 } 834 835 GtkWidget* child = gtk_bin_get_child(GTK_BIN(widget)); 836 if (child) 837 gtk_container_propagate_expose(GTK_CONTAINER(widget), child, e); 838 839 return TRUE; 840 } 841 842 void DownloadItemGtk::OnClick(GtkWidget* widget) { 843 UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download", 844 base::Time::Now() - creation_time_); 845 get_download()->OpenDownload(); 846 parent_shelf_->ItemOpened(); 847 } 848 849 gboolean DownloadItemGtk::OnButtonPress(GtkWidget* button, 850 GdkEventButton* event) { 851 if (event->type == GDK_BUTTON_PRESS && event->button == 3) { 852 ShowPopupMenu(NULL, event); 853 return TRUE; 854 } 855 856 return FALSE; 857 } 858 859 gboolean DownloadItemGtk::OnProgressAreaExpose(GtkWidget* widget, 860 GdkEventExpose* event) { 861 // Create a transparent canvas. 862 gfx::CanvasSkiaPaint canvas(event, false); 863 if (complete_animation_.is_animating()) { 864 if (get_download()->IsInterrupted()) { 865 download_util::PaintDownloadInterrupted(&canvas, 866 widget->allocation.x, widget->allocation.y, 867 complete_animation_.GetCurrentValue(), 868 download_util::SMALL); 869 } else { 870 download_util::PaintDownloadComplete(&canvas, 871 widget->allocation.x, widget->allocation.y, 872 complete_animation_.GetCurrentValue(), 873 download_util::SMALL); 874 } 875 } else if (get_download()->IsInProgress()) { 876 download_util::PaintDownloadProgress(&canvas, 877 widget->allocation.x, widget->allocation.y, 878 progress_angle_, 879 get_download()->PercentComplete(), 880 download_util::SMALL); 881 } 882 883 // |icon_small_| may be NULL if it is still loading. If the file is an 884 // unrecognized type then we will get back a generic system icon. Hence 885 // there is no need to use the chromium-specific default download item icon. 886 if (icon_small_) { 887 const int offset = download_util::kSmallProgressIconOffset; 888 canvas.DrawBitmapInt(*icon_small_, 889 widget->allocation.x + offset, widget->allocation.y + offset); 890 } 891 892 return TRUE; 893 } 894 895 gboolean DownloadItemGtk::OnMenuButtonPressEvent(GtkWidget* button, 896 GdkEventButton* event) { 897 if (event->type == GDK_BUTTON_PRESS && event->button == 1) { 898 ShowPopupMenu(button, event); 899 menu_showing_ = true; 900 gtk_widget_queue_draw(button); 901 return TRUE; 902 } 903 904 return FALSE; 905 } 906 907 void DownloadItemGtk::ShowPopupMenu(GtkWidget* button, 908 GdkEventButton* event) { 909 // Stop any completion animation. 910 if (complete_animation_.is_animating()) 911 complete_animation_.End(); 912 913 if (!menu_.get()) 914 menu_.reset(new DownloadShelfContextMenuGtk(download_model_.get(), this)); 915 menu_->Popup(button, event); 916 } 917 918 gboolean DownloadItemGtk::OnDangerousPromptExpose(GtkWidget* widget, 919 GdkEventExpose* event) { 920 if (!theme_service_->UseGtkTheme()) { 921 // The hbox renderer will take care of the border when in GTK mode. 922 dangerous_nine_box_->RenderToWidget(widget); 923 } 924 return FALSE; // Continue propagation. 925 } 926 927 void DownloadItemGtk::OnDangerousAccept(GtkWidget* button) { 928 UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download", 929 base::Time::Now() - creation_time_); 930 get_download()->DangerousDownloadValidated(); 931 } 932 933 void DownloadItemGtk::OnDangerousDecline(GtkWidget* button) { 934 UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download", 935 base::Time::Now() - creation_time_); 936 if (get_download()->IsPartialDownload()) 937 get_download()->Cancel(true); 938 get_download()->Delete(DownloadItem::DELETE_DUE_TO_USER_DISCARD); 939 } 940