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