Home | History | Annotate | Download | only in download
      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