root/chrome/browser/ui/gtk/download/download_item_gtk.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. weak_ptr_factory_
  2. OnDownloadUpdated
  3. OnDownloadDestroyed
  4. AnimationProgressed
  5. Observe
  6. UpdateDownloadProgress
  7. StartDownloadProgress
  8. StopDownloadProgress
  9. OnLoadSmallIconComplete
  10. OnLoadLargeIconComplete
  11. LoadIcon
  12. UpdateTooltip
  13. UpdateNameLabel
  14. UpdateStatusLabel
  15. UpdateDangerWarning
  16. UpdateDangerIcon
  17. InitNineBoxes
  18. OnHboxExpose
  19. OnExpose
  20. ReenableHbox
  21. OnDownloadOpened
  22. OnClick
  23. OnButtonPress
  24. OnProgressAreaExpose
  25. OnMenuButtonPressEvent
  26. ShowPopupMenu
  27. OnDangerousPromptExpose
  28. OnDangerousAccept
  29. OnDangerousDecline

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/gtk/download/download_item_gtk.h"

#include "base/basictypes.h"
#include "base/callback.h"
#include "base/debug/trace_event.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_stats.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/gtk/download/download_item_drag.h"
#include "chrome/browser/ui/gtk/download/download_shelf_context_menu_gtk.h"
#include "chrome/browser/ui/gtk/download/download_shelf_gtk.h"
#include "chrome/browser/ui/gtk/gtk_theme_service.h"
#include "chrome/browser/ui/gtk/gtk_util.h"
#include "chrome/browser/ui/gtk/nine_box.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/notification_source.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas_skia_paint.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/skia_utils_gtk.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"

namespace {

// The width of the |menu_button_| widget. It has to be at least as wide as the
// bitmap that we use to draw it, i.e. 16, but can be more.
const int kMenuButtonWidth = 16;

// Padding on left and right of items in dangerous download prompt.
const int kDangerousElementPadding = 3;

// Minimum width of the dangerous download message at which we will start
// wrapping.
const int kDangerousTextWidth = 200;

// Amount of space we allot to showing the filename. If the filename is too wide
// it will be elided.
const int kTextWidth = 140;

// We only cap the size of the tooltip so we don't crash.
const int kTooltipMaxWidth = 1000;

// The minimum width we will ever draw the download item. Used as a lower bound
// during animation. This number comes from the width of the images used to
// make the download item.
const int kMinDownloadItemWidth = DownloadShelf::kSmallProgressIconSize;

// New download item animation speed in milliseconds.
const int kNewItemAnimationDurationMs = 800;

// How long the 'download complete/interrupted' animation should last for.
const int kCompleteAnimationDurationMs = 2500;

// Height of the body.
const int kBodyHeight = DownloadShelf::kSmallProgressIconSize;

// Width of the body area of the download item.
// TODO(estade): get rid of the fudge factor. http://crbug.com/18692
const int kBodyWidth = kTextWidth + 50 + DownloadShelf::kSmallProgressIconSize;

// The font size of the text, and that size rounded down to the nearest integer
// for the size of the arrow in GTK theme mode.
const double kTextSize = 13.4;  // 13.4px == 10pt @ 96dpi

// Darken light-on-dark download status text by 20% before drawing, thus
// creating a "muted" version of title text for both dark-on-light and
// light-on-dark themes.
static const double kDownloadItemLuminanceMod = 0.8;

// How long we keep the item disabled after the user clicked it to open the
// downloaded item.
static const int kDisabledOnOpenDurationMs = 3000;

}  // namespace

NineBox* DownloadItemGtk::body_nine_box_normal_ = NULL;
NineBox* DownloadItemGtk::body_nine_box_prelight_ = NULL;
NineBox* DownloadItemGtk::body_nine_box_active_ = NULL;

NineBox* DownloadItemGtk::menu_nine_box_normal_ = NULL;
NineBox* DownloadItemGtk::menu_nine_box_prelight_ = NULL;
NineBox* DownloadItemGtk::menu_nine_box_active_ = NULL;

NineBox* DownloadItemGtk::dangerous_nine_box_ = NULL;

using content::DownloadItem;

DownloadItemGtk::DownloadItemGtk(DownloadShelfGtk* parent_shelf,
                                 DownloadItem* download_item)
    : parent_shelf_(parent_shelf),
      arrow_(NULL),
      menu_showing_(false),
      theme_service_(
          GtkThemeService::GetFrom(parent_shelf->browser()->profile())),
      progress_angle_(DownloadShelf::kStartAngleDegrees),
      download_model_(download_item),
      dangerous_prompt_(NULL),
      dangerous_label_(NULL),
      complete_animation_(this),
      icon_small_(NULL),
      icon_large_(NULL),
      creation_time_(base::Time::Now()),
      download_complete_(false),
      disabled_while_opening_(false),
      weak_ptr_factory_(this) {
  LoadIcon();

  body_.Own(gtk_button_new());
  gtk_widget_set_app_paintable(body_.get(), TRUE);
  UpdateTooltip();

  g_signal_connect(body_.get(), "expose-event",
                   G_CALLBACK(OnExposeThunk), this);
  g_signal_connect(body_.get(), "clicked",
                   G_CALLBACK(OnClickThunk), this);
  g_signal_connect(body_.get(), "button-press-event",
                   G_CALLBACK(OnButtonPressThunk), this);
  gtk_widget_set_can_focus(body_.get(), FALSE);
  // Remove internal padding on the button.
  GtkRcStyle* no_padding_style = gtk_rc_style_new();
  no_padding_style->xthickness = 0;
  no_padding_style->ythickness = 0;
  gtk_widget_modify_style(body_.get(), no_padding_style);
  g_object_unref(no_padding_style);

  name_label_ = gtk_label_new(NULL);
  // Left align and vertically center the labels.
  gtk_misc_set_alignment(GTK_MISC(name_label_), 0, 0.5);
  // Until we switch to vector graphics, force the font size.
  gtk_util::ForceFontSizePixels(name_label_, kTextSize);

  UpdateNameLabel();

  status_label_ = NULL;

  // Stack the labels on top of one another.
  text_stack_ = gtk_vbox_new(FALSE, 0);
  g_signal_connect(text_stack_, "destroy",
                   G_CALLBACK(gtk_widget_destroyed), &text_stack_);
  gtk_box_pack_start(GTK_BOX(text_stack_), name_label_, TRUE, TRUE, 0);

  // We use a GtkFixed because we don't want it to have its own window.
  // This choice of widget is not critically important though.
  progress_area_.Own(gtk_fixed_new());
  gtk_widget_set_size_request(progress_area_.get(),
      DownloadShelf::kSmallProgressIconSize,
      DownloadShelf::kSmallProgressIconSize);
  gtk_widget_set_app_paintable(progress_area_.get(), TRUE);
  g_signal_connect(progress_area_.get(), "expose-event",
                   G_CALLBACK(OnProgressAreaExposeThunk), this);

  // Put the download progress icon on the left of the labels.
  GtkWidget* body_hbox = gtk_hbox_new(FALSE, 0);
  gtk_container_add(GTK_CONTAINER(body_.get()), body_hbox);
  gtk_box_pack_start(GTK_BOX(body_hbox), progress_area_.get(), FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(body_hbox), text_stack_, TRUE, TRUE, 0);

  menu_button_ = gtk_button_new();
  gtk_widget_set_app_paintable(menu_button_, TRUE);
  gtk_widget_set_can_focus(menu_button_, FALSE);
  g_signal_connect(menu_button_, "expose-event",
                   G_CALLBACK(OnExposeThunk), this);
  g_signal_connect(menu_button_, "button-press-event",
                   G_CALLBACK(OnMenuButtonPressEventThunk), this);
  g_object_set_data(G_OBJECT(menu_button_), "left-align-popup",
                    reinterpret_cast<void*>(true));

  GtkWidget* shelf_hbox = parent_shelf->GetHBox();
  hbox_.Own(gtk_hbox_new(FALSE, 0));
  g_signal_connect(hbox_.get(), "expose-event",
                   G_CALLBACK(OnHboxExposeThunk), this);
  gtk_box_pack_start(GTK_BOX(hbox_.get()), body_.get(), FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(hbox_.get()), menu_button_, FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(shelf_hbox), hbox_.get(), FALSE, FALSE, 0);
  // Insert as the leftmost item.
  gtk_box_reorder_child(GTK_BOX(shelf_hbox), hbox_.get(), 0);

  download()->AddObserver(this);

  new_item_animation_.reset(new gfx::SlideAnimation(this));
  new_item_animation_->SetSlideDuration(kNewItemAnimationDurationMs);
  gtk_widget_show_all(hbox_.get());

  if (download_model_.IsDangerous()) {
    RecordDangerousDownloadWarningShown(download()->GetDangerType());

    // Hide the download item components for now.
    gtk_widget_set_no_show_all(body_.get(), TRUE);
    gtk_widget_set_no_show_all(menu_button_, TRUE);
    gtk_widget_hide(body_.get());
    gtk_widget_hide(menu_button_);

    // Create an hbox to hold it all.
    dangerous_hbox_.Own(gtk_hbox_new(FALSE, kDangerousElementPadding));

    // Add padding at the beginning and end. The hbox will add padding between
    // the empty labels and the other elements.
    GtkWidget* empty_label_a = gtk_label_new(NULL);
    GtkWidget* empty_label_b = gtk_label_new(NULL);
    gtk_box_pack_start(GTK_BOX(dangerous_hbox_.get()), empty_label_a,
                       FALSE, FALSE, 0);
    gtk_box_pack_end(GTK_BOX(dangerous_hbox_.get()), empty_label_b,
                     FALSE, FALSE, 0);

    // Create the warning icon.
    dangerous_image_ = gtk_image_new();
    gtk_box_pack_start(GTK_BOX(dangerous_hbox_.get()), dangerous_image_,
                       FALSE, FALSE, 0);

    dangerous_label_ = gtk_label_new(NULL);
    // We pass TRUE, TRUE so that the label will condense to less than its
    // request when the animation is going on.
    gtk_box_pack_start(GTK_BOX(dangerous_hbox_.get()), dangerous_label_,
                       TRUE, TRUE, 0);

    // Create the nevermind button.
    GtkWidget* dangerous_decline = gtk_button_new_with_label(
        l10n_util::GetStringUTF8(IDS_DISCARD_DOWNLOAD).c_str());
    g_signal_connect(dangerous_decline, "clicked",
                     G_CALLBACK(OnDangerousDeclineThunk), this);
    gtk_util::CenterWidgetInHBox(dangerous_hbox_.get(), dangerous_decline,
                                 false, 0);

    // Create the ok button, if this is the kind that can be bypassed.
    if (!download_model_.IsMalicious()) {
      GtkWidget* dangerous_accept = gtk_button_new_with_label(
          base::UTF16ToUTF8(
              download_model_.GetWarningConfirmButtonText()).c_str());
      g_signal_connect(dangerous_accept, "clicked",
                       G_CALLBACK(OnDangerousAcceptThunk), this);
      gtk_util::CenterWidgetInHBox(
          dangerous_hbox_.get(), dangerous_accept, false, 0);
    }

    // Put it in an alignment so that padding will be added on the left and
    // right.
    dangerous_prompt_ = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
    gtk_alignment_set_padding(GTK_ALIGNMENT(dangerous_prompt_),
        0, 0, kDangerousElementPadding, kDangerousElementPadding);
    gtk_container_add(GTK_CONTAINER(dangerous_prompt_), dangerous_hbox_.get());
    gtk_box_pack_start(GTK_BOX(hbox_.get()), dangerous_prompt_, FALSE, FALSE,
                       0);
    gtk_widget_set_app_paintable(dangerous_prompt_, TRUE);
    gtk_widget_set_redraw_on_allocate(dangerous_prompt_, TRUE);
    g_signal_connect(dangerous_prompt_, "expose-event",
                     G_CALLBACK(OnDangerousPromptExposeThunk), this);
    gtk_widget_show_all(dangerous_prompt_);
  }

  registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
                 content::Source<ThemeService>(theme_service_));
  theme_service_->InitThemesFor(this);

  // Set the initial width of the widget to be animated.
  if (download_model_.IsDangerous()) {
    gtk_widget_set_size_request(dangerous_hbox_.get(),
                                dangerous_hbox_start_width_, -1);
  } else {
    gtk_widget_set_size_request(body_.get(), kMinDownloadItemWidth, -1);
  }

  new_item_animation_->Show();

  complete_animation_.SetTweenType(gfx::Tween::LINEAR);
  complete_animation_.SetSlideDuration(kCompleteAnimationDurationMs);

  // Update the status text and animation state.
  OnDownloadUpdated(download());
}

DownloadItemGtk::~DownloadItemGtk() {
  // First close the menu and then destroy the GtkWidgets. Bug#97724
  if (menu_.get())
    menu_.reset();

  StopDownloadProgress();
  download()->RemoveObserver(this);

  // We may free some shelf space for showing more download items.
  parent_shelf_->MaybeShowMoreDownloadItems();

  hbox_.Destroy();
  progress_area_.Destroy();
  body_.Destroy();
  dangerous_hbox_.Destroy();

  // Make sure this widget has been destroyed and the pointer we hold to it
  // NULLed.
  DCHECK(!status_label_);
}

void DownloadItemGtk::OnDownloadUpdated(DownloadItem* download_item) {
  DCHECK_EQ(download(), download_item);

  if (dangerous_prompt_ != NULL && !download_model_.IsDangerous()) {
    // We have been approved.
    gtk_widget_set_no_show_all(body_.get(), FALSE);
    gtk_widget_set_no_show_all(menu_button_, FALSE);
    gtk_widget_show_all(hbox_.get());
    gtk_widget_destroy(dangerous_prompt_);
    gtk_widget_set_size_request(body_.get(), kBodyWidth, -1);
    dangerous_prompt_ = NULL;

    // We may free some shelf space for showing more download items.
    parent_shelf_->MaybeShowMoreDownloadItems();
  }

  if (download()->GetTargetFilePath() != icon_filepath_) {
    LoadIcon();
    UpdateTooltip();
  }

  switch (download()->GetState()) {
    case DownloadItem::CANCELLED:
      StopDownloadProgress();
      gtk_widget_queue_draw(progress_area_.get());
      break;
    case DownloadItem::INTERRUPTED:
      StopDownloadProgress();
      UpdateTooltip();

      complete_animation_.Show();
      break;
    case DownloadItem::COMPLETE:
      // ShouldRemoveFromShelfWhenComplete() may change after the download's
      // initial transition to COMPLETE, so we check it before the idemopotency
      // shield below.
      if (download_model_.ShouldRemoveFromShelfWhenComplete()) {
        parent_shelf_->RemoveDownloadItem(this);  // This will delete us!
        return;
      }

      // We've already handled the completion specific actions; skip
      // doing the non-idempotent ones again.
      if (download_complete_)
        break;

      StopDownloadProgress();

      // Set up the widget as a drag source.
      DownloadItemDrag::SetSource(body_.get(), download(), icon_large_);

      complete_animation_.Show();
      download_complete_ = true;
      break;
    case DownloadItem::IN_PROGRESS:
      download()->IsPaused() ?
          StopDownloadProgress() : StartDownloadProgress();
      break;
    default:
      NOTREACHED();
  }

  status_text_ = base::UTF16ToUTF8(download_model_.GetStatusText());
  UpdateStatusLabel(status_text_);
}

void DownloadItemGtk::OnDownloadDestroyed(DownloadItem* download_item) {
  DCHECK_EQ(download(), download_item);
  parent_shelf_->RemoveDownloadItem(this);
  // This will delete us!
}

void DownloadItemGtk::AnimationProgressed(const gfx::Animation* animation) {
  if (animation == &complete_animation_) {
    gtk_widget_queue_draw(progress_area_.get());
  } else {
    DCHECK(animation == new_item_animation_.get());
    if (download_model_.IsDangerous()) {
      int progress = static_cast<int>((dangerous_hbox_full_width_ -
                                       dangerous_hbox_start_width_) *
                                      animation->GetCurrentValue());
      int showing_width = dangerous_hbox_start_width_ + progress;
      gtk_widget_set_size_request(dangerous_hbox_.get(), showing_width, -1);
    } else {
      int showing_width = std::max(kMinDownloadItemWidth,
          static_cast<int>(kBodyWidth * animation->GetCurrentValue()));
      gtk_widget_set_size_request(body_.get(), showing_width, -1);
    }
  }
}

void DownloadItemGtk::Observe(int type,
                              const content::NotificationSource& source,
                              const content::NotificationDetails& details) {
  if (type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED) {
    // Our GtkArrow is only visible in gtk mode. Otherwise, we let the custom
    // rendering code do whatever it wants.
    if (theme_service_->UsingNativeTheme()) {
      if (!arrow_) {
        arrow_ = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE);
        gtk_widget_set_size_request(arrow_,
                                    static_cast<int>(kTextSize),
                                    static_cast<int>(kTextSize));
        gtk_container_add(GTK_CONTAINER(menu_button_), arrow_);
      }

      gtk_widget_set_size_request(menu_button_, -1, -1);
      gtk_widget_show(arrow_);
    } else {
      InitNineBoxes();

      gtk_widget_set_size_request(menu_button_, kMenuButtonWidth, 0);

      if (arrow_)
        gtk_widget_hide(arrow_);
    }

    UpdateNameLabel();
    UpdateStatusLabel(status_text_);
    UpdateDangerWarning();
  }
}

// Download progress animation functions.

void DownloadItemGtk::UpdateDownloadProgress() {
  progress_angle_ =
      (progress_angle_ + DownloadShelf::kUnknownIncrementDegrees) %
      DownloadShelf::kMaxDegrees;
  gtk_widget_queue_draw(progress_area_.get());
}

void DownloadItemGtk::StartDownloadProgress() {
  if (progress_timer_.IsRunning())
    return;
  progress_timer_.Start(FROM_HERE,
      base::TimeDelta::FromMilliseconds(DownloadShelf::kProgressRateMs), this,
      &DownloadItemGtk::UpdateDownloadProgress);
}

void DownloadItemGtk::StopDownloadProgress() {
  progress_timer_.Stop();
}

// Icon loading functions.

void DownloadItemGtk::OnLoadSmallIconComplete(gfx::Image* image) {
  icon_small_ = image;
  gtk_widget_queue_draw(progress_area_.get());
}

void DownloadItemGtk::OnLoadLargeIconComplete(gfx::Image* image) {
  icon_large_ = image;
  if (download()->GetState() == DownloadItem::COMPLETE)
    DownloadItemDrag::SetSource(body_.get(), download(), icon_large_);
  // Else, the download will be made draggable once an OnDownloadUpdated()
  // notification is received with a download in COMPLETE state.
}

void DownloadItemGtk::LoadIcon() {
  cancelable_task_tracker_.TryCancelAll();
  IconManager* im = g_browser_process->icon_manager();
  icon_filepath_ = download()->GetTargetFilePath();
  im->LoadIcon(icon_filepath_,
               IconLoader::SMALL,
               base::Bind(&DownloadItemGtk::OnLoadSmallIconComplete,
                          base::Unretained(this)),
               &cancelable_task_tracker_);
  im->LoadIcon(icon_filepath_,
               IconLoader::LARGE,
               base::Bind(&DownloadItemGtk::OnLoadLargeIconComplete,
                          base::Unretained(this)),
               &cancelable_task_tracker_);
}

void DownloadItemGtk::UpdateTooltip() {
  const gfx::FontList& font_list =
      ui::ResourceBundle::GetSharedInstance().GetFontList(
          ui::ResourceBundle::BaseFont);
  base::string16 tooltip_text =
      download_model_.GetTooltipText(font_list, kTooltipMaxWidth);
  gtk_widget_set_tooltip_text(body_.get(),
                              base::UTF16ToUTF8(tooltip_text).c_str());
}

void DownloadItemGtk::UpdateNameLabel() {
  const gfx::FontList& font_list =
      ui::ResourceBundle::GetSharedInstance().GetFontList(
          ui::ResourceBundle::BaseFont);
  base::string16 filename;
  if (!disabled_while_opening_) {
    filename = gfx::ElideFilename(
        download()->GetFileNameToReportUser(), font_list, kTextWidth);
  } else {
    // First, Calculate the download status opening string width.
    base::string16 status_string =
        l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
                                   base::string16());
    int status_string_width = gfx::GetStringWidth(status_string, font_list);
    // Then, elide the file name.
    base::string16 filename_string =
        gfx::ElideFilename(download()->GetFileNameToReportUser(), font_list,
                          kTextWidth - status_string_width);
    // Last, concat the whole string.
    filename = l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
                                                 filename_string);
  }

  GdkColor color = theme_service_->GetGdkColor(
      ThemeProperties::COLOR_BOOKMARK_TEXT);
  gtk_util::SetLabelColor(
      name_label_,
      theme_service_->UsingNativeTheme() ? NULL : &color);
  gtk_label_set_text(GTK_LABEL(name_label_),
                     base::UTF16ToUTF8(filename).c_str());
}

void DownloadItemGtk::UpdateStatusLabel(const std::string& status_text) {
  if (!text_stack_) {
    // At least our container has been destroyed, which means that
    // this item is on the way to being destroyed; don't do anything.
    return;
  }

  // If |status_text| is empty, only |name_label_| is displayed at the
  // vertical center of |text_stack_|. Otherwise, |name_label_| is displayed
  // on the upper half of |text_stack_| and |status_label_| is displayed
  // on the lower half of |text_stack_|.
  if (status_text.empty()) {
    if (status_label_)
      gtk_widget_destroy(status_label_);
    return;
  }
  if (!status_label_) {
    status_label_ = gtk_label_new(NULL);
    g_signal_connect(status_label_, "destroy",
                     G_CALLBACK(gtk_widget_destroyed), &status_label_);
    // Left align and vertically center the labels.
    gtk_misc_set_alignment(GTK_MISC(status_label_), 0, 0.5);
    // Until we switch to vector graphics, force the font size.
    gtk_util::ForceFontSizePixels(status_label_, kTextSize);

    gtk_box_pack_start(GTK_BOX(text_stack_), status_label_, FALSE, FALSE, 0);
    gtk_widget_show_all(status_label_);
  }

  GdkColor text_color;
  if (!theme_service_->UsingNativeTheme()) {
    SkColor color = theme_service_->GetColor(
        ThemeProperties::COLOR_BOOKMARK_TEXT);
    if (color_utils::RelativeLuminance(color) > 0.5) {
      color = SkColorSetRGB(
          static_cast<int>(kDownloadItemLuminanceMod *
                           SkColorGetR(color)),
          static_cast<int>(kDownloadItemLuminanceMod *
                           SkColorGetG(color)),
          static_cast<int>(kDownloadItemLuminanceMod *
                           SkColorGetB(color)));
    }

    // Lighten the color by blending it with the download item body color. These
    // values are taken from IDR_DOWNLOAD_BUTTON.
    SkColor blend_color = SkColorSetRGB(241, 245, 250);
    text_color = gfx::SkColorToGdkColor(
        color_utils::AlphaBlend(blend_color, color, 77));
  }

  gtk_util::SetLabelColor(
      status_label_,
      theme_service_->UsingNativeTheme() ? NULL : &text_color);
  gtk_label_set_text(GTK_LABEL(status_label_), status_text.c_str());
}

void DownloadItemGtk::UpdateDangerWarning() {
  if (dangerous_prompt_) {
    UpdateDangerIcon();

    // We create |dangerous_warning| as a wide string so we can more easily
    // calculate its length in characters.
    const gfx::FontList& font_list =
        ui::ResourceBundle::GetSharedInstance().GetFontList(
            ui::ResourceBundle::BaseFont);
    base::string16 dangerous_warning =
        download_model_.GetWarningText(font_list, kTextWidth);
    if (theme_service_->UsingNativeTheme()) {
      gtk_util::SetLabelColor(dangerous_label_, NULL);
    } else {
      GdkColor color = theme_service_->GetGdkColor(
          ThemeProperties::COLOR_BOOKMARK_TEXT);
      gtk_util::SetLabelColor(dangerous_label_, &color);
    }

    gtk_label_set_text(GTK_LABEL(dangerous_label_),
                       base::UTF16ToUTF8(dangerous_warning).c_str());

    // Until we switch to vector graphics, force the font size.
    gtk_util::ForceFontSizePixels(dangerous_label_, kTextSize);

    gtk_widget_set_size_request(dangerous_label_, -1, -1);
    gtk_label_set_line_wrap(GTK_LABEL(dangerous_label_), FALSE);

    GtkRequisition req;
    gtk_widget_size_request(dangerous_label_, &req);

    gint label_width = req.width;
    if (req.width > kDangerousTextWidth) {
      // If the label width exceeds kDangerousTextWidth, we try line wrapping
      // starting at 60% and increasing in 10% intervals of the full width until
      // we have a label that fits within the height constraints of the shelf.
      gtk_label_set_line_wrap(GTK_LABEL(dangerous_label_), TRUE);
      int full_width = req.width;
      int tenths = 6;
      do {
        label_width = full_width * tenths / 10;
        gtk_widget_set_size_request(dangerous_label_, label_width, -1);
        gtk_widget_size_request(dangerous_label_, &req);
      } while (req.height > kBodyHeight && ++tenths <= 10);
      DCHECK(req.height <= kBodyHeight);
    }

    // The width will depend on the text. We must do this each time we possibly
    // change the label above.
    gtk_widget_size_request(dangerous_hbox_.get(), &req);
    dangerous_hbox_full_width_ = req.width;
    dangerous_hbox_start_width_ = dangerous_hbox_full_width_ - label_width;
  }
}

void DownloadItemGtk::UpdateDangerIcon() {
  if (theme_service_->UsingNativeTheme()) {
    const char* stock = download_model_.MightBeMalicious() ?
        GTK_STOCK_DIALOG_ERROR : GTK_STOCK_DIALOG_WARNING;
    gtk_image_set_from_stock(
        GTK_IMAGE(dangerous_image_), stock, GTK_ICON_SIZE_SMALL_TOOLBAR);
  } else {
    // Set the warning icon.
    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    int pixbuf_id =
        download_model_.MightBeMalicious() ? IDR_SAFEBROWSING_WARNING
                                           : IDR_WARNING;
    gtk_image_set_from_pixbuf(GTK_IMAGE(dangerous_image_),
                              rb.GetNativeImageNamed(pixbuf_id).ToGdkPixbuf());
  }
}

// static
void DownloadItemGtk::InitNineBoxes() {
  if (body_nine_box_normal_)
    return;

  body_nine_box_normal_ = new NineBox(
      IDR_DOWNLOAD_BUTTON_LEFT_TOP,
      IDR_DOWNLOAD_BUTTON_CENTER_TOP,
      IDR_DOWNLOAD_BUTTON_RIGHT_TOP,
      IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE,
      IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE,
      IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE,
      IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM,
      IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM,
      IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM);

  body_nine_box_prelight_ = new NineBox(
      IDR_DOWNLOAD_BUTTON_LEFT_TOP_H,
      IDR_DOWNLOAD_BUTTON_CENTER_TOP_H,
      IDR_DOWNLOAD_BUTTON_RIGHT_TOP_H,
      IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_H,
      IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_H,
      IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_H,
      IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_H,
      IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_H,
      IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_H);

  body_nine_box_active_ = new NineBox(
      IDR_DOWNLOAD_BUTTON_LEFT_TOP_P,
      IDR_DOWNLOAD_BUTTON_CENTER_TOP_P,
      IDR_DOWNLOAD_BUTTON_RIGHT_TOP_P,
      IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_P,
      IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_P,
      IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_P,
      IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_P,
      IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_P,
      IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_P);

  menu_nine_box_normal_ = new NineBox(
      IDR_DOWNLOAD_BUTTON_MENU_TOP, 0, 0,
      IDR_DOWNLOAD_BUTTON_MENU_MIDDLE, 0, 0,
      IDR_DOWNLOAD_BUTTON_MENU_BOTTOM, 0, 0);

  menu_nine_box_prelight_ = new NineBox(
      IDR_DOWNLOAD_BUTTON_MENU_TOP_H, 0, 0,
      IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_H, 0, 0,
      IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_H, 0, 0);

  menu_nine_box_active_ = new NineBox(
      IDR_DOWNLOAD_BUTTON_MENU_TOP_P, 0, 0,
      IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_P, 0, 0,
      IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_P, 0, 0);

  dangerous_nine_box_ = new NineBox(
      IDR_DOWNLOAD_BUTTON_LEFT_TOP,
      IDR_DOWNLOAD_BUTTON_CENTER_TOP,
      IDR_DOWNLOAD_BUTTON_RIGHT_TOP_NO_DD,
      IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE,
      IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE,
      IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_NO_DD,
      IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM,
      IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM,
      IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_NO_DD);
}

gboolean DownloadItemGtk::OnHboxExpose(GtkWidget* widget, GdkEventExpose* e) {
  TRACE_EVENT0("ui::gtk", "DownloadItemGtk::OnHboxExpose");
  if (theme_service_->UsingNativeTheme()) {
    GtkAllocation allocation;
    gtk_widget_get_allocation(widget, &allocation);
    int border_width = gtk_container_get_border_width(GTK_CONTAINER(widget));
    int x = allocation.x + border_width;
    int y = allocation.y + border_width;
    int width = allocation.width - border_width * 2;
    int height = allocation.height - border_width * 2;

    if (download_model_.IsDangerous()) {
      // Draw a simple frame around the area when we're displaying the warning.
      gtk_paint_shadow(gtk_widget_get_style(widget),
                       gtk_widget_get_window(widget),
                       gtk_widget_get_state(widget),
                       static_cast<GtkShadowType>(GTK_SHADOW_OUT),
                       &e->area, widget, "frame",
                       x, y, width, height);
    } else {
      // Manually draw the GTK button border around the download item. We draw
      // the left part of the button (the file), a divider, and then the right
      // part of the button (the menu). We can't draw a button on top of each
      // other (*cough*Clearlooks*cough*) so instead, to draw the left part of
      // the button, we instruct GTK to draw the entire button...with a
      // doctored clip rectangle to the left part of the button sans
      // separator. We then repeat this for the right button.
      GtkStyle* style = gtk_widget_get_style(body_.get());

      GtkAllocation left_clip;
      gtk_widget_get_allocation(body_.get(), &left_clip);

      GtkAllocation right_clip;
      gtk_widget_get_allocation(menu_button_, &right_clip);

      GtkShadowType body_shadow =
          GTK_BUTTON(body_.get())->depressed ? GTK_SHADOW_IN : GTK_SHADOW_OUT;
      gtk_paint_box(style,
                    gtk_widget_get_window(widget),
                    gtk_widget_get_state(body_.get()),
                    body_shadow,
                    &left_clip, widget, "button",
                    x, y, width, height);

      GtkShadowType menu_shadow =
          GTK_BUTTON(menu_button_)->depressed ? GTK_SHADOW_IN : GTK_SHADOW_OUT;
      gtk_paint_box(style,
                    gtk_widget_get_window(widget),
                    gtk_widget_get_state(menu_button_),
                    menu_shadow,
                    &right_clip, widget, "button",
                    x, y, width, height);

      // Doing the math to reverse engineer where we should be drawing our line
      // is hard and relies on copying GTK internals, so instead steal the
      // allocation of the gtk arrow which is close enough (and will error on
      // the conservative side).
      GtkAllocation arrow_allocation;
      gtk_widget_get_allocation(arrow_, &arrow_allocation);
      gtk_paint_vline(style,
                      gtk_widget_get_window(widget),
                      gtk_widget_get_state(widget),
                      &e->area, widget, "button",
                      arrow_allocation.y,
                      arrow_allocation.y + arrow_allocation.height,
                      left_clip.x + left_clip.width);
    }
  }
  return FALSE;
}

gboolean DownloadItemGtk::OnExpose(GtkWidget* widget, GdkEventExpose* e) {
  TRACE_EVENT0("ui::gtk", "DownloadItemGtk::OnExpose");
  if (!theme_service_->UsingNativeTheme()) {
    bool is_body = widget == body_.get();

    NineBox* nine_box = NULL;
    // If true, this widget is |body_|, otherwise it is |menu_button_|.
    if (gtk_widget_get_state(widget) == GTK_STATE_PRELIGHT)
      nine_box = is_body ? body_nine_box_prelight_ : menu_nine_box_prelight_;
    else if (gtk_widget_get_state(widget) == GTK_STATE_ACTIVE)
      nine_box = is_body ? body_nine_box_active_ : menu_nine_box_active_;
    else
      nine_box = is_body ? body_nine_box_normal_ : menu_nine_box_normal_;

    // When the button is showing, we want to draw it as active. We have to do
    // this explicitly because the button's state will be NORMAL while the menu
    // has focus.
    if (!is_body && menu_showing_)
      nine_box = menu_nine_box_active_;

    nine_box->RenderToWidget(widget);
  }

  GtkWidget* child = gtk_bin_get_child(GTK_BIN(widget));
  if (child)
    gtk_container_propagate_expose(GTK_CONTAINER(widget), child, e);

  return TRUE;
}

void DownloadItemGtk::ReenableHbox() {
  gtk_widget_set_sensitive(hbox_.get(), true);
  disabled_while_opening_ = false;
  UpdateNameLabel();
}

void DownloadItemGtk::OnDownloadOpened(DownloadItem* download) {
  disabled_while_opening_ = true;
  gtk_widget_set_sensitive(hbox_.get(), false);
  base::MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      base::Bind(&DownloadItemGtk::ReenableHbox,
                 weak_ptr_factory_.GetWeakPtr()),
      base::TimeDelta::FromMilliseconds(kDisabledOnOpenDurationMs));
  UpdateNameLabel();
  parent_shelf_->ItemOpened();
}

void DownloadItemGtk::OnClick(GtkWidget* widget) {
  UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
                           base::Time::Now() - creation_time_);
  download()->OpenDownload();
}

gboolean DownloadItemGtk::OnButtonPress(GtkWidget* button,
                                        GdkEventButton* event) {
  if (event->type == GDK_BUTTON_PRESS && event->button == 3) {
    ShowPopupMenu(NULL, event);
    return TRUE;
  }
  return FALSE;
}

gboolean DownloadItemGtk::OnProgressAreaExpose(GtkWidget* widget,
                                               GdkEventExpose* event) {
  TRACE_EVENT0("ui::gtk", "DownloadItemGtk::OnProgressAreaExpose");

  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);

  // Create a transparent canvas.
  gfx::CanvasSkiaPaint canvas(event, false);
  DownloadItem::DownloadState state = download()->GetState();
  if (complete_animation_.is_animating()) {
    if (state == DownloadItem::INTERRUPTED) {
      DownloadShelf::PaintDownloadInterrupted(
          &canvas,
          allocation.x,
          allocation.y,
          complete_animation_.GetCurrentValue(),
          DownloadShelf::SMALL);
    } else {
      DownloadShelf::PaintDownloadComplete(
          &canvas,
          allocation.x,
          allocation.y,
          complete_animation_.GetCurrentValue(),
          DownloadShelf::SMALL);
    }
  } else if (state == DownloadItem::IN_PROGRESS) {
    DownloadShelf::PaintDownloadProgress(&canvas,
                                         allocation.x,
                                         allocation.y,
                                         progress_angle_,
                                         download_model_.PercentComplete(),
                                         DownloadShelf::SMALL);
  }

  // |icon_small_| may be NULL if it is still loading. If the file is an
  // unrecognized type then we will get back a generic system icon. Hence
  // there is no need to use the chromium-specific default download item icon.
  if (icon_small_) {
    const int offset = DownloadShelf::kSmallProgressIconOffset;
    canvas.DrawImageInt(icon_small_->AsImageSkia(),
        allocation.x + offset, allocation.y + offset);
  }

  return TRUE;
}

gboolean DownloadItemGtk::OnMenuButtonPressEvent(GtkWidget* button,
                                                 GdkEventButton* event) {
  if (event->type == GDK_BUTTON_PRESS && event->button == 1) {
    ShowPopupMenu(button, event);
    menu_showing_ = true;
    gtk_widget_queue_draw(button);
    return TRUE;
  }
  return FALSE;
}

void DownloadItemGtk::ShowPopupMenu(GtkWidget* button,
                                    GdkEventButton* event) {
  // Stop any completion animation.
  if (complete_animation_.is_animating())
    complete_animation_.End();

  if (!menu_.get()) {
    menu_.reset(new DownloadShelfContextMenuGtk(this,
                                                parent_shelf_->GetNavigator()));
  }
  menu_->Popup(button, event);
}

gboolean DownloadItemGtk::OnDangerousPromptExpose(GtkWidget* widget,
                                                  GdkEventExpose* event) {
  TRACE_EVENT0("ui::gtk", "DownloadItemGtk::OnDangerousPromptExpose");
  if (!theme_service_->UsingNativeTheme()) {
    // The hbox renderer will take care of the border when in GTK mode.
    dangerous_nine_box_->RenderToWidget(widget);
  }
  return FALSE;  // Continue propagation.
}

void DownloadItemGtk::OnDangerousAccept(GtkWidget* button) {
  UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
                           base::Time::Now() - creation_time_);
  download()->ValidateDangerousDownload();
}

void DownloadItemGtk::OnDangerousDecline(GtkWidget* button) {
  UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
                           base::Time::Now() - creation_time_);
  download()->Remove();
}

/* [<][>][^][v][top][bottom][index][help] */