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