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