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