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
42namespace {
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.
46const int kMenuButtonWidth = 16;
47
48// Padding on left and right of items in dangerous download prompt.
49const int kDangerousElementPadding = 3;
50
51// Minimum width of the dangerous download message at which we will start
52// wrapping.
53const 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.
57const int kTextWidth = 140;
58
59// We only cap the size of the tooltip so we don't crash.
60const 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.
65const int kMinDownloadItemWidth = DownloadShelf::kSmallProgressIconSize;
66
67// New download item animation speed in milliseconds.
68const int kNewItemAnimationDurationMs = 800;
69
70// How long the 'download complete/interrupted' animation should last for.
71const int kCompleteAnimationDurationMs = 2500;
72
73// Height of the body.
74const 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
78const 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.
82const 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.
87static 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.
91static const int kDisabledOnOpenDurationMs = 3000;
92
93}  // namespace
94
95NineBox* DownloadItemGtk::body_nine_box_normal_ = NULL;
96NineBox* DownloadItemGtk::body_nine_box_prelight_ = NULL;
97NineBox* DownloadItemGtk::body_nine_box_active_ = NULL;
98
99NineBox* DownloadItemGtk::menu_nine_box_normal_ = NULL;
100NineBox* DownloadItemGtk::menu_nine_box_prelight_ = NULL;
101NineBox* DownloadItemGtk::menu_nine_box_active_ = NULL;
102
103NineBox* DownloadItemGtk::dangerous_nine_box_ = NULL;
104
105using content::DownloadItem;
106
107DownloadItemGtk::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
285DownloadItemGtk::~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
306void 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
372void DownloadItemGtk::OnDownloadDestroyed(DownloadItem* download_item) {
373  DCHECK_EQ(download(), download_item);
374  parent_shelf_->RemoveDownloadItem(this);
375  // This will delete us!
376}
377
378void 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
397void 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
431void DownloadItemGtk::UpdateDownloadProgress() {
432  progress_angle_ =
433      (progress_angle_ + DownloadShelf::kUnknownIncrementDegrees) %
434      DownloadShelf::kMaxDegrees;
435  gtk_widget_queue_draw(progress_area_.get());
436}
437
438void 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
446void DownloadItemGtk::StopDownloadProgress() {
447  progress_timer_.Stop();
448}
449
450// Icon loading functions.
451
452void DownloadItemGtk::OnLoadSmallIconComplete(gfx::Image* image) {
453  icon_small_ = image;
454  gtk_widget_queue_draw(progress_area_.get());
455}
456
457void 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
465void 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
481void 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
487void 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
520void 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
576void 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
628void 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
645void 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
709gboolean 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
780gboolean 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
810void DownloadItemGtk::ReenableHbox() {
811  gtk_widget_set_sensitive(hbox_.get(), true);
812  disabled_while_opening_ = false;
813  UpdateNameLabel();
814}
815
816void 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
828void DownloadItemGtk::OnClick(GtkWidget* widget) {
829  UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
830                           base::Time::Now() - creation_time_);
831  download()->OpenDownload();
832}
833
834gboolean 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
843gboolean 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
890gboolean 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
901void 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
914gboolean 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
924void DownloadItemGtk::OnDangerousAccept(GtkWidget* button) {
925  UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
926                           base::Time::Now() - creation_time_);
927  download()->ValidateDangerousDownload();
928}
929
930void DownloadItemGtk::OnDangerousDecline(GtkWidget* button) {
931  UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
932                           base::Time::Now() - creation_time_);
933  download()->Remove();
934}
935