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/views/download/download_item_view.h"
6
7#include <vector>
8
9#include "base/callback.h"
10#include "base/file_path.h"
11#include "base/i18n/break_iterator.h"
12#include "base/i18n/rtl.h"
13#include "base/metrics/histogram.h"
14#include "base/string_util.h"
15#include "base/sys_string_conversions.h"
16#include "base/utf_string_conversions.h"
17#include "chrome/browser/browser_process.h"
18#include "chrome/browser/download/download_item_model.h"
19#include "chrome/browser/download/download_util.h"
20#include "chrome/browser/themes/theme_service.h"
21#include "chrome/browser/ui/views/download/download_shelf_view.h"
22#include "grit/generated_resources.h"
23#include "grit/theme_resources.h"
24#include "ui/base/accessibility/accessible_view_state.h"
25#include "ui/base/animation/slide_animation.h"
26#include "ui/base/l10n/l10n_util.h"
27#include "ui/base/resource/resource_bundle.h"
28#include "ui/base/text/text_elider.h"
29#include "ui/gfx/canvas_skia.h"
30#include "ui/gfx/color_utils.h"
31#include "ui/gfx/image.h"
32#include "unicode/uchar.h"
33#include "views/controls/button/native_button.h"
34#include "views/controls/menu/menu_2.h"
35#include "views/widget/root_view.h"
36#include "views/widget/widget.h"
37
38using base::TimeDelta;
39
40// TODO(paulg): These may need to be adjusted when download progress
41//              animation is added, and also possibly to take into account
42//              different screen resolutions.
43static const int kTextWidth = 140;            // Pixels
44static const int kDangerousTextWidth = 200;   // Pixels
45static const int kHorizontalTextPadding = 2;  // Pixels
46static const int kVerticalPadding = 3;        // Pixels
47static const int kVerticalTextSpacer = 2;     // Pixels
48static const int kVerticalTextPadding = 2;    // Pixels
49
50// The maximum number of characters we show in a file name when displaying the
51// dangerous download message.
52static const int kFileNameMaxLength = 20;
53
54// We add some padding before the left image so that the progress animation icon
55// hides the corners of the left image.
56static const int kLeftPadding = 0;  // Pixels.
57
58// The space between the Save and Discard buttons when prompting for a dangerous
59// download.
60static const int kButtonPadding = 5;  // Pixels.
61
62// The space on the left and right side of the dangerous download label.
63static const int kLabelPadding = 4;  // Pixels.
64
65static const SkColor kFileNameDisabledColor = SkColorSetRGB(171, 192, 212);
66
67// How long the 'download complete' animation should last for.
68static const int kCompleteAnimationDurationMs = 2500;
69
70// How long the 'download interrupted' animation should last for.
71static const int kInterruptedAnimationDurationMs = 2500;
72
73// How long we keep the item disabled after the user clicked it to open the
74// downloaded item.
75static const int kDisabledOnOpenDuration = 3000;
76
77// Darken light-on-dark download status text by 20% before drawing, thus
78// creating a "muted" version of title text for both dark-on-light and
79// light-on-dark themes.
80static const double kDownloadItemLuminanceMod = 0.8;
81
82// DownloadShelfContextMenuWin -------------------------------------------------
83
84class DownloadShelfContextMenuWin : public DownloadShelfContextMenu {
85 public:
86  explicit DownloadShelfContextMenuWin(BaseDownloadItemModel* model)
87      : DownloadShelfContextMenu(model) {
88    DCHECK(model);
89  }
90
91  void Run(const gfx::Point& point) {
92    if (download_->IsComplete())
93      menu_.reset(new views::Menu2(GetFinishedMenuModel()));
94    else
95      menu_.reset(new views::Menu2(GetInProgressMenuModel()));
96
97    // The menu's alignment is determined based on the UI layout.
98    views::Menu2::Alignment alignment;
99    if (base::i18n::IsRTL())
100      alignment = views::Menu2::ALIGN_TOPRIGHT;
101    else
102      alignment = views::Menu2::ALIGN_TOPLEFT;
103    menu_->RunMenuAt(point, alignment);
104  }
105
106  // This method runs when the caller has been deleted and we should not attempt
107  // to access |download_|.
108  void Stop() {
109    download_ = NULL;
110  }
111
112 private:
113  scoped_ptr<views::Menu2> menu_;
114};
115
116// DownloadItemView ------------------------------------------------------------
117
118DownloadItemView::DownloadItemView(DownloadItem* download,
119    DownloadShelfView* parent,
120    BaseDownloadItemModel* model)
121  : warning_icon_(NULL),
122    download_(download),
123    parent_(parent),
124    status_text_(UTF16ToWide(
125        l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING))),
126    show_status_text_(true),
127    body_state_(NORMAL),
128    drop_down_state_(NORMAL),
129    progress_angle_(download_util::kStartAngleDegrees),
130    drop_down_pressed_(false),
131    dragging_(false),
132    starting_drag_(false),
133    model_(model),
134    save_button_(NULL),
135    discard_button_(NULL),
136    dangerous_download_label_(NULL),
137    dangerous_download_label_sized_(false),
138    disabled_while_opening_(false),
139    creation_time_(base::Time::Now()),
140    ALLOW_THIS_IN_INITIALIZER_LIST(reenable_method_factory_(this)),
141    deleted_(NULL) {
142  DCHECK(download_);
143  download_->AddObserver(this);
144
145  ResourceBundle &rb = ResourceBundle::GetSharedInstance();
146
147  BodyImageSet normal_body_image_set = {
148    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP),
149    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE),
150    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM),
151    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP),
152    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE),
153    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM),
154    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP),
155    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE),
156    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM)
157  };
158  normal_body_image_set_ = normal_body_image_set;
159
160  DropDownImageSet normal_drop_down_image_set = {
161    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP),
162    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE),
163    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM)
164  };
165  normal_drop_down_image_set_ = normal_drop_down_image_set;
166
167  BodyImageSet hot_body_image_set = {
168    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP_H),
169    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_H),
170    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_H),
171    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP_H),
172    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_H),
173    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_H),
174    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_H),
175    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_H),
176    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_H)
177  };
178  hot_body_image_set_ = hot_body_image_set;
179
180  DropDownImageSet hot_drop_down_image_set = {
181    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP_H),
182    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_H),
183    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_H)
184  };
185  hot_drop_down_image_set_ = hot_drop_down_image_set;
186
187  BodyImageSet pushed_body_image_set = {
188    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP_P),
189    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_P),
190    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_P),
191    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP_P),
192    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_P),
193    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_P),
194    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_P),
195    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_P),
196    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_P)
197  };
198  pushed_body_image_set_ = pushed_body_image_set;
199
200  DropDownImageSet pushed_drop_down_image_set = {
201    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP_P),
202    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_P),
203    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_P)
204  };
205  pushed_drop_down_image_set_ = pushed_drop_down_image_set;
206
207  BodyImageSet dangerous_mode_body_image_set = {
208    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP),
209    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE),
210    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM),
211    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP),
212    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE),
213    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM),
214    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_NO_DD),
215    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_NO_DD),
216    rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_NO_DD)
217  };
218  dangerous_mode_body_image_set_ = dangerous_mode_body_image_set;
219
220  LoadIcon();
221  tooltip_text_ =
222      UTF16ToWide(download_->GetFileNameToReportUser().LossyDisplayName());
223
224  font_ = ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::BaseFont);
225  box_height_ = std::max<int>(2 * kVerticalPadding + font_.GetHeight() +
226                                  kVerticalTextPadding + font_.GetHeight(),
227                              2 * kVerticalPadding +
228                                  normal_body_image_set_.top_left->height() +
229                                  normal_body_image_set_.bottom_left->height());
230
231  if (download_util::kSmallProgressIconSize > box_height_)
232    box_y_ = (download_util::kSmallProgressIconSize - box_height_) / 2;
233  else
234    box_y_ = kVerticalPadding;
235
236  gfx::Size size = GetPreferredSize();
237  if (base::i18n::IsRTL()) {
238    // Drop down button is glued to the left of the download shelf.
239    drop_down_x_left_ = 0;
240    drop_down_x_right_ = normal_drop_down_image_set_.top->width();
241  } else {
242    // Drop down button is glued to the right of the download shelf.
243    drop_down_x_left_ =
244        size.width() - normal_drop_down_image_set_.top->width();
245    drop_down_x_right_ = size.width();
246  }
247
248  body_hover_animation_.reset(new ui::SlideAnimation(this));
249  drop_hover_animation_.reset(new ui::SlideAnimation(this));
250
251  if (download->safety_state() == DownloadItem::DANGEROUS) {
252    tooltip_text_.clear();
253    body_state_ = DANGEROUS;
254    drop_down_state_ = DANGEROUS;
255    save_button_ = new views::NativeButton(this,
256        UTF16ToWide(l10n_util::GetStringUTF16(
257            download->is_extension_install() ?
258                IDS_CONTINUE_EXTENSION_DOWNLOAD : IDS_SAVE_DOWNLOAD)));
259    save_button_->set_ignore_minimum_size(true);
260    discard_button_ = new views::NativeButton(
261        this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_DISCARD_DOWNLOAD)));
262    discard_button_->set_ignore_minimum_size(true);
263    AddChildView(save_button_);
264    AddChildView(discard_button_);
265
266    // Ensure the file name is not too long.
267
268    // Extract the file extension (if any).
269    FilePath filename(download->target_name());
270#if defined(OS_LINUX)
271    string16 extension = WideToUTF16(base::SysNativeMBToWide(
272        filename.Extension()));
273#else
274    string16 extension = filename.Extension();
275#endif
276
277    // Remove leading '.'
278    if (extension.length() > 0)
279      extension = extension.substr(1);
280#if defined(OS_LINUX)
281    string16 rootname = WideToUTF16(base::SysNativeMBToWide(
282        filename.RemoveExtension().value()));
283#else
284    string16 rootname = filename.RemoveExtension().value();
285#endif
286
287    // Elide giant extensions (this shouldn't currently be hit, but might
288    // in future, should we ever notice unsafe giant extensions).
289    if (extension.length() > kFileNameMaxLength / 2)
290      ui::ElideString(extension, kFileNameMaxLength / 2, &extension);
291
292    // The dangerous download label text and icon are different
293    // under different cases.
294    string16 dangerous_label;
295    if (download->danger_type() == DownloadItem::DANGEROUS_URL) {
296      // Safebrowsing shows the download URL leads to malicious file.
297      warning_icon_ = rb.GetBitmapNamed(IDR_SAFEBROWSING_WARNING);
298      dangerous_label =
299          l10n_util::GetStringUTF16(IDS_PROMPT_UNSAFE_DOWNLOAD_URL);
300    } else {
301      // The download file has dangerous file type (e.g.: an executable).
302      DCHECK(download->danger_type() == DownloadItem::DANGEROUS_FILE);
303      warning_icon_ = rb.GetBitmapNamed(IDR_WARNING);
304      if (download->is_extension_install()) {
305        dangerous_label =
306            l10n_util::GetStringUTF16(IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION);
307      } else {
308        ui::ElideString(rootname,
309                        kFileNameMaxLength - extension.length(),
310                        &rootname);
311        string16 filename = rootname + ASCIIToUTF16(".") + extension;
312        filename = base::i18n::GetDisplayStringInLTRDirectionality(filename);
313        dangerous_label =
314            l10n_util::GetStringFUTF16(IDS_PROMPT_DANGEROUS_DOWNLOAD, filename);
315      }
316    }
317
318    dangerous_download_label_ = new views::Label(UTF16ToWide(dangerous_label));
319    dangerous_download_label_->SetMultiLine(true);
320    dangerous_download_label_->SetHorizontalAlignment(
321        views::Label::ALIGN_LEFT);
322    AddChildView(dangerous_download_label_);
323    SizeLabelToMinWidth();
324  }
325
326  UpdateAccessibleName();
327  set_accessibility_focusable(true);
328
329  // Set up our animation.
330  StartDownloadProgress();
331}
332
333DownloadItemView::~DownloadItemView() {
334  if (context_menu_.get()) {
335    context_menu_->Stop();
336  }
337  icon_consumer_.CancelAllRequests();
338  StopDownloadProgress();
339  download_->RemoveObserver(this);
340  if (deleted_)
341    *deleted_ = true;
342}
343
344// Progress animation handlers.
345
346void DownloadItemView::UpdateDownloadProgress() {
347  progress_angle_ = (progress_angle_ +
348                     download_util::kUnknownIncrementDegrees) %
349                    download_util::kMaxDegrees;
350  SchedulePaint();
351}
352
353void DownloadItemView::StartDownloadProgress() {
354  if (progress_timer_.IsRunning())
355    return;
356  progress_timer_.Start(
357      TimeDelta::FromMilliseconds(download_util::kProgressRateMs), this,
358      &DownloadItemView::UpdateDownloadProgress);
359}
360
361void DownloadItemView::StopDownloadProgress() {
362  progress_timer_.Stop();
363}
364
365void DownloadItemView::OnExtractIconComplete(IconManager::Handle handle,
366                                             gfx::Image* icon_bitmap) {
367  if (icon_bitmap)
368    parent()->SchedulePaint();
369}
370
371// DownloadObserver interface.
372
373// Update the progress graphic on the icon and our text status label
374// to reflect our current bytes downloaded, time remaining.
375void DownloadItemView::OnDownloadUpdated(DownloadItem* download) {
376  DCHECK(download == download_);
377
378  if (body_state_ == DANGEROUS &&
379      download->safety_state() == DownloadItem::DANGEROUS_BUT_VALIDATED) {
380    // We have been approved.
381    ClearDangerousMode();
382  }
383
384  string16 status_text = model_->GetStatusText();
385  switch (download_->state()) {
386    case DownloadItem::IN_PROGRESS:
387      download_->is_paused() ? StopDownloadProgress() : StartDownloadProgress();
388      break;
389    case DownloadItem::INTERRUPTED:
390      StopDownloadProgress();
391      complete_animation_.reset(new ui::SlideAnimation(this));
392      complete_animation_->SetSlideDuration(kInterruptedAnimationDurationMs);
393      complete_animation_->SetTweenType(ui::Tween::LINEAR);
394      complete_animation_->Show();
395      if (status_text.empty())
396        show_status_text_ = false;
397      SchedulePaint();
398      LoadIcon();
399      break;
400    case DownloadItem::COMPLETE:
401      if (download_->auto_opened()) {
402        parent_->RemoveDownloadView(this);  // This will delete us!
403        return;
404      }
405      StopDownloadProgress();
406      complete_animation_.reset(new ui::SlideAnimation(this));
407      complete_animation_->SetSlideDuration(kCompleteAnimationDurationMs);
408      complete_animation_->SetTweenType(ui::Tween::LINEAR);
409      complete_animation_->Show();
410      if (status_text.empty())
411        show_status_text_ = false;
412      SchedulePaint();
413      LoadIcon();
414      break;
415    case DownloadItem::CANCELLED:
416      StopDownloadProgress();
417      LoadIcon();
418      break;
419    case DownloadItem::REMOVING:
420      parent_->RemoveDownloadView(this);  // This will delete us!
421      return;
422    default:
423      NOTREACHED();
424  }
425
426  status_text_ = UTF16ToWideHack(status_text);
427  UpdateAccessibleName();
428
429  // We use the parent's (DownloadShelfView's) SchedulePaint, since there
430  // are spaces between each DownloadItemView that the parent is responsible
431  // for painting.
432  parent()->SchedulePaint();
433}
434
435void DownloadItemView::OnDownloadOpened(DownloadItem* download) {
436  disabled_while_opening_ = true;
437  SetEnabled(false);
438  MessageLoop::current()->PostDelayedTask(
439      FROM_HERE,
440      reenable_method_factory_.NewRunnableMethod(&DownloadItemView::Reenable),
441      kDisabledOnOpenDuration);
442
443  // Notify our parent.
444  parent_->OpenedDownload(this);
445}
446
447// View overrides
448
449// In dangerous mode we have to layout our buttons.
450void DownloadItemView::Layout() {
451  if (IsDangerousMode()) {
452    dangerous_download_label_->SetColor(
453      GetThemeProvider()->GetColor(ThemeService::COLOR_BOOKMARK_TEXT));
454
455    int x = kLeftPadding + dangerous_mode_body_image_set_.top_left->width() +
456      warning_icon_->width() + kLabelPadding;
457    int y = (height() - dangerous_download_label_->height()) / 2;
458    dangerous_download_label_->SetBounds(x, y,
459                                         dangerous_download_label_->width(),
460                                         dangerous_download_label_->height());
461    gfx::Size button_size = GetButtonSize();
462    x += dangerous_download_label_->width() + kLabelPadding;
463    y = (height() - button_size.height()) / 2;
464    save_button_->SetBounds(x, y, button_size.width(), button_size.height());
465    x += button_size.width() + kButtonPadding;
466    discard_button_->SetBounds(x, y, button_size.width(), button_size.height());
467  }
468}
469
470gfx::Size DownloadItemView::GetPreferredSize() {
471  int width, height;
472
473  // First, we set the height to the height of two rows or text plus margins.
474  height = 2 * kVerticalPadding + 2 * font_.GetHeight() + kVerticalTextPadding;
475  // Then we increase the size if the progress icon doesn't fit.
476  height = std::max<int>(height, download_util::kSmallProgressIconSize);
477
478  if (IsDangerousMode()) {
479    width = kLeftPadding + dangerous_mode_body_image_set_.top_left->width();
480    width += warning_icon_->width() + kLabelPadding;
481    width += dangerous_download_label_->width() + kLabelPadding;
482    gfx::Size button_size = GetButtonSize();
483    // Make sure the button fits.
484    height = std::max<int>(height, 2 * kVerticalPadding + button_size.height());
485    // Then we make sure the warning icon fits.
486    height = std::max<int>(height, 2 * kVerticalPadding +
487                                   warning_icon_->height());
488    width += button_size.width() * 2 + kButtonPadding;
489    width += dangerous_mode_body_image_set_.top_right->width();
490  } else {
491    width = kLeftPadding + normal_body_image_set_.top_left->width();
492    width += download_util::kSmallProgressIconSize;
493    width += kTextWidth;
494    width += normal_body_image_set_.top_right->width();
495    width += normal_drop_down_image_set_.top->width();
496  }
497  return gfx::Size(width, height);
498}
499
500// Handle a mouse click and open the context menu if the mouse is
501// over the drop-down region.
502bool DownloadItemView::OnMousePressed(const views::MouseEvent& event) {
503  // Mouse should not activate us in dangerous mode.
504  if (IsDangerousMode())
505    return true;
506
507  // Stop any completion animation.
508  if (complete_animation_.get() && complete_animation_->is_animating())
509    complete_animation_->End();
510
511  gfx::Point menu_location(event.location());
512  if (event.IsOnlyLeftMouseButton()) {
513    if (!InDropDownButtonXCoordinateRange(event.x())) {
514      SetState(PUSHED, NORMAL);
515      return true;
516    }
517
518    // Anchor the menu below the dropmarker.
519    menu_location.SetPoint(base::i18n::IsRTL() ?
520                               drop_down_x_right_ : drop_down_x_left_,
521                           height());
522    drop_down_pressed_ = true;
523    SetState(NORMAL, PUSHED);
524  }
525  ShowContextMenu(menu_location, true);
526  return true;
527}
528
529// Handle drag (file copy) operations.
530bool DownloadItemView::OnMouseDragged(const views::MouseEvent& event) {
531  // Mouse should not activate us in dangerous mode.
532  if (IsDangerousMode())
533    return true;
534
535  if (!starting_drag_) {
536    starting_drag_ = true;
537    drag_start_point_ = event.location();
538  }
539  if (dragging_) {
540    if (download_->IsComplete()) {
541      IconManager* im = g_browser_process->icon_manager();
542      gfx::Image* icon = im->LookupIcon(download_->GetUserVerifiedFilePath(),
543                                        IconLoader::SMALL);
544      if (icon) {
545        views::Widget* widget = GetWidget();
546        download_util::DragDownload(download_, icon,
547                                    widget ? widget->GetNativeView() : NULL);
548      }
549    }
550  } else if (ExceededDragThreshold(
551                 event.location().x() - drag_start_point_.x(),
552                 event.location().y() - drag_start_point_.y())) {
553    dragging_ = true;
554  }
555  return true;
556}
557
558void DownloadItemView::OnMouseReleased(const views::MouseEvent& event) {
559  // Mouse should not activate us in dangerous mode.
560  if (IsDangerousMode())
561    return;
562
563  if (event.IsOnlyLeftMouseButton() &&
564      !InDropDownButtonXCoordinateRange(event.x())) {
565    OpenDownload();
566  }
567
568  SetState(NORMAL, NORMAL);
569}
570
571void DownloadItemView::OnMouseCaptureLost() {
572  // Mouse should not activate us in dangerous mode.
573  if (IsDangerousMode())
574    return;
575
576  if (dragging_) {
577    // Starting a drag results in a MouseCaptureLost.
578    dragging_ = false;
579    starting_drag_ = false;
580  } else {
581    SetState(NORMAL, NORMAL);
582  }
583}
584
585void DownloadItemView::OnMouseMoved(const views::MouseEvent& event) {
586  // Mouse should not activate us in dangerous mode.
587  if (IsDangerousMode())
588    return;
589
590  bool on_body = !InDropDownButtonXCoordinateRange(event.x());
591  SetState(on_body ? HOT : NORMAL, on_body ? NORMAL : HOT);
592  if (on_body) {
593    body_hover_animation_->Show();
594    drop_hover_animation_->Hide();
595  } else {
596    body_hover_animation_->Hide();
597    drop_hover_animation_->Show();
598  }
599}
600
601void DownloadItemView::OnMouseExited(const views::MouseEvent& event) {
602  // Mouse should not activate us in dangerous mode.
603  if (IsDangerousMode())
604    return;
605
606  SetState(NORMAL, drop_down_pressed_ ? PUSHED : NORMAL);
607  body_hover_animation_->Hide();
608  drop_hover_animation_->Hide();
609}
610
611bool DownloadItemView::OnKeyPressed(const views::KeyEvent& event) {
612  // Key press should not activate us in dangerous mode.
613  if (IsDangerousMode())
614    return true;
615
616  if (event.key_code() == ui::VKEY_SPACE ||
617      event.key_code() == ui::VKEY_RETURN) {
618    OpenDownload();
619    return true;
620  }
621  return false;
622}
623
624bool DownloadItemView::GetTooltipText(const gfx::Point& p,
625                                      std::wstring* tooltip) {
626  if (tooltip_text_.empty())
627    return false;
628
629  tooltip->assign(tooltip_text_);
630  return true;
631}
632
633void DownloadItemView::ShowContextMenu(const gfx::Point& p,
634                                       bool is_mouse_gesture) {
635  gfx::Point point = p;
636
637  // Similar hack as in MenuButton.
638  // We're about to show the menu from a mouse press. By showing from the
639  // mouse press event we block RootView in mouse dispatching. This also
640  // appears to cause RootView to get a mouse pressed BEFORE the mouse
641  // release is seen, which means RootView sends us another mouse press no
642  // matter where the user pressed. To force RootView to recalculate the
643  // mouse target during the mouse press we explicitly set the mouse handler
644  // to NULL.
645  GetRootView()->SetMouseHandler(NULL);
646
647  // If |is_mouse_gesture| is false, |p| is ignored. The menu is shown aligned
648  // to drop down arrow button.
649  if (!is_mouse_gesture) {
650    drop_down_pressed_ = true;
651    SetState(NORMAL, PUSHED);
652
653    point.set_y(height());
654    if (base::i18n::IsRTL())
655      point.set_x(drop_down_x_right_);
656    else
657      point.set_x(drop_down_x_left_);
658  }
659
660  views::View::ConvertPointToScreen(this, &point);
661
662  if (!context_menu_.get())
663    context_menu_.reset(new DownloadShelfContextMenuWin(model_.get()));
664  // When we call the Run method on the menu, it runs an inner message loop
665  // that might causes us to be deleted.
666  bool deleted = false;
667  deleted_ = &deleted;
668  context_menu_->Run(point);
669  if (deleted)
670    return;  // We have been deleted! Don't access 'this'.
671  deleted_ = NULL;
672
673  // If the menu action was to remove the download, this view will also be
674  // invalid so we must not access 'this' in this case.
675  if (context_menu_->download()) {
676    drop_down_pressed_ = false;
677    // Showing the menu blocks. Here we revert the state.
678    SetState(NORMAL, NORMAL);
679  }
680}
681
682void DownloadItemView::GetAccessibleState(ui::AccessibleViewState* state) {
683  state->name = accessible_name_;
684  state->role = ui::AccessibilityTypes::ROLE_PUSHBUTTON;
685  if (download_->safety_state() == DownloadItem::DANGEROUS) {
686    state->state = ui::AccessibilityTypes::STATE_UNAVAILABLE;
687  } else {
688    state->state = ui::AccessibilityTypes::STATE_HASPOPUP;
689  }
690}
691
692void DownloadItemView::ButtonPressed(
693    views::Button* sender, const views::Event& event) {
694  if (sender == discard_button_) {
695    UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
696                             base::Time::Now() - creation_time_);
697    if (download_->IsPartialDownload())
698      download_->Cancel(true);
699    download_->Delete(DownloadItem::DELETE_DUE_TO_USER_DISCARD);
700    // WARNING: we are deleted at this point.  Don't access 'this'.
701  } else if (sender == save_button_) {
702    // The user has confirmed a dangerous download.  We'd record how quickly the
703    // user did this to detect whether we're being clickjacked.
704    UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
705                             base::Time::Now() - creation_time_);
706    // This will change the state and notify us.
707    download_->DangerousDownloadValidated();
708  }
709}
710
711void DownloadItemView::AnimationProgressed(const ui::Animation* animation) {
712  // We don't care if what animation (body button/drop button/complete),
713  // is calling back, as they all have to go through the same paint call.
714  SchedulePaint();
715}
716
717void DownloadItemView::OnPaint(gfx::Canvas* canvas) {
718  BodyImageSet* body_image_set = NULL;
719  switch (body_state_) {
720    case NORMAL:
721    case HOT:
722      body_image_set = &normal_body_image_set_;
723      break;
724    case PUSHED:
725      body_image_set = &pushed_body_image_set_;
726      break;
727    case DANGEROUS:
728      body_image_set = &dangerous_mode_body_image_set_;
729      break;
730    default:
731      NOTREACHED();
732  }
733  DropDownImageSet* drop_down_image_set = NULL;
734  switch (drop_down_state_) {
735    case NORMAL:
736    case HOT:
737      drop_down_image_set = &normal_drop_down_image_set_;
738      break;
739    case PUSHED:
740      drop_down_image_set = &pushed_drop_down_image_set_;
741      break;
742    case DANGEROUS:
743      drop_down_image_set = NULL;  // No drop-down in dangerous mode.
744      break;
745    default:
746      NOTREACHED();
747  }
748
749  int center_width = width() - kLeftPadding -
750                     body_image_set->left->width() -
751                     body_image_set->right->width() -
752                     (drop_down_image_set ?
753                        normal_drop_down_image_set_.center->width() :
754                        0);
755
756  // May be caused by animation.
757  if (center_width <= 0)
758    return;
759
760  // Draw status before button image to effectively lighten text.
761  if (!IsDangerousMode()) {
762    if (show_status_text_) {
763      int mirrored_x = GetMirroredXWithWidthInView(
764          download_util::kSmallProgressIconSize, kTextWidth);
765      // Add font_.height() to compensate for title, which is drawn later.
766      int y = box_y_ + kVerticalPadding + font_.GetHeight() +
767              kVerticalTextPadding;
768      SkColor file_name_color = GetThemeProvider()->GetColor(
769          ThemeService::COLOR_BOOKMARK_TEXT);
770      // If text is light-on-dark, lightening it alone will do nothing.
771      // Therefore we mute luminance a wee bit before drawing in this case.
772      if (color_utils::RelativeLuminance(file_name_color) > 0.5)
773          file_name_color = SkColorSetRGB(
774              static_cast<int>(kDownloadItemLuminanceMod *
775                               SkColorGetR(file_name_color)),
776              static_cast<int>(kDownloadItemLuminanceMod *
777                               SkColorGetG(file_name_color)),
778              static_cast<int>(kDownloadItemLuminanceMod *
779                               SkColorGetB(file_name_color)));
780      canvas->DrawStringInt(WideToUTF16Hack(status_text_), font_,
781                            file_name_color, mirrored_x, y, kTextWidth,
782                            font_.GetHeight());
783    }
784  }
785
786  // Paint the background images.
787  int x = kLeftPadding;
788  canvas->Save();
789  if (base::i18n::IsRTL()) {
790    // Since we do not have the mirrored images for
791    // (hot_)body_image_set->top_left, (hot_)body_image_set->left,
792    // (hot_)body_image_set->bottom_left, and drop_down_image_set,
793    // for RTL UI, we flip the canvas to draw those images mirrored.
794    // Consequently, we do not need to mirror the x-axis of those images.
795    canvas->TranslateInt(width(), 0);
796    canvas->ScaleInt(-1, 1);
797  }
798  PaintBitmaps(canvas,
799               body_image_set->top_left, body_image_set->left,
800               body_image_set->bottom_left,
801               x, box_y_, box_height_, body_image_set->top_left->width());
802  x += body_image_set->top_left->width();
803  PaintBitmaps(canvas,
804               body_image_set->top, body_image_set->center,
805               body_image_set->bottom,
806               x, box_y_, box_height_, center_width);
807  x += center_width;
808  PaintBitmaps(canvas,
809               body_image_set->top_right, body_image_set->right,
810               body_image_set->bottom_right,
811               x, box_y_, box_height_, body_image_set->top_right->width());
812
813  // Overlay our body hot state.
814  if (body_hover_animation_->GetCurrentValue() > 0) {
815    canvas->SaveLayerAlpha(
816        static_cast<int>(body_hover_animation_->GetCurrentValue() * 255));
817    canvas->AsCanvasSkia()->drawARGB(0, 255, 255, 255, SkXfermode::kClear_Mode);
818
819    int x = kLeftPadding;
820    PaintBitmaps(canvas,
821                 hot_body_image_set_.top_left, hot_body_image_set_.left,
822                 hot_body_image_set_.bottom_left,
823                 x, box_y_, box_height_, hot_body_image_set_.top_left->width());
824    x += body_image_set->top_left->width();
825    PaintBitmaps(canvas,
826                 hot_body_image_set_.top, hot_body_image_set_.center,
827                 hot_body_image_set_.bottom,
828                 x, box_y_, box_height_, center_width);
829    x += center_width;
830    PaintBitmaps(canvas,
831                 hot_body_image_set_.top_right, hot_body_image_set_.right,
832                 hot_body_image_set_.bottom_right,
833                 x, box_y_, box_height_,
834                 hot_body_image_set_.top_right->width());
835    canvas->Restore();
836  }
837
838  x += body_image_set->top_right->width();
839
840  // Paint the drop-down.
841  if (drop_down_image_set) {
842    PaintBitmaps(canvas,
843                 drop_down_image_set->top, drop_down_image_set->center,
844                 drop_down_image_set->bottom,
845                 x, box_y_, box_height_, drop_down_image_set->top->width());
846
847    // Overlay our drop-down hot state.
848    if (drop_hover_animation_->GetCurrentValue() > 0) {
849      canvas->SaveLayerAlpha(
850          static_cast<int>(drop_hover_animation_->GetCurrentValue() * 255));
851      canvas->AsCanvasSkia()->drawARGB(0, 255, 255, 255,
852                                       SkXfermode::kClear_Mode);
853
854      PaintBitmaps(canvas,
855                   drop_down_image_set->top, drop_down_image_set->center,
856                   drop_down_image_set->bottom,
857                   x, box_y_, box_height_, drop_down_image_set->top->width());
858
859      canvas->Restore();
860    }
861  }
862
863  // Restore the canvas to avoid file name etc. text are drawn flipped.
864  // Consequently, the x-axis of following canvas->DrawXXX() method should be
865  // mirrored so the text and images are down in the right positions.
866  canvas->Restore();
867
868  // Print the text, left aligned and always print the file extension.
869  // Last value of x was the end of the right image, just before the button.
870  // Note that in dangerous mode we use a label (as the text is multi-line).
871  if (!IsDangerousMode()) {
872    string16 filename;
873    if (!disabled_while_opening_) {
874      filename = ui::ElideFilename(download_->GetFileNameToReportUser(),
875                                   font_, kTextWidth);
876    } else {
877      // First, Calculate the download status opening string width.
878      string16 status_string =
879          l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING, string16());
880      int status_string_width = font_.GetStringWidth(status_string);
881      // Then, elide the file name.
882      string16 filename_string =
883          ui::ElideFilename(download_->GetFileNameToReportUser(), font_,
884                            kTextWidth - status_string_width);
885      // Last, concat the whole string.
886      filename = l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
887                                            filename_string);
888    }
889
890    int mirrored_x = GetMirroredXWithWidthInView(
891        download_util::kSmallProgressIconSize, kTextWidth);
892    SkColor file_name_color = GetThemeProvider()->GetColor(
893        ThemeService::COLOR_BOOKMARK_TEXT);
894    int y =
895        box_y_ + (show_status_text_ ? kVerticalPadding :
896                                      (box_height_ - font_.GetHeight()) / 2);
897
898    // Draw the file's name.
899    canvas->DrawStringInt(filename, font_,
900                          IsEnabled() ? file_name_color :
901                                        kFileNameDisabledColor,
902                          mirrored_x, y, kTextWidth, font_.GetHeight());
903  }
904
905  // Load the icon.
906  IconManager* im = g_browser_process->icon_manager();
907  gfx::Image* image = im->LookupIcon(download_->GetUserVerifiedFilePath(),
908                                     IconLoader::SMALL);
909  const SkBitmap* icon = NULL;
910  if (IsDangerousMode())
911    icon = warning_icon_;
912  else if (image)
913    icon = *image;
914
915  // We count on the fact that the icon manager will cache the icons and if one
916  // is available, it will be cached here. We *don't* want to request the icon
917  // to be loaded here, since this will also get called if the icon can't be
918  // loaded, in which case LookupIcon will always be NULL. The loading will be
919  // triggered only when we think the status might change.
920  if (icon) {
921    if (!IsDangerousMode()) {
922      if (download_->IsInProgress()) {
923        download_util::PaintDownloadProgress(canvas, this, 0, 0,
924                                             progress_angle_,
925                                             download_->PercentComplete(),
926                                             download_util::SMALL);
927      } else if (download_->IsComplete() &&
928                 complete_animation_.get() &&
929                 complete_animation_->is_animating()) {
930        if (download_->IsInterrupted()) {
931          download_util::PaintDownloadInterrupted(canvas, this, 0, 0,
932              complete_animation_->GetCurrentValue(),
933              download_util::SMALL);
934        } else {
935          download_util::PaintDownloadComplete(canvas, this, 0, 0,
936              complete_animation_->GetCurrentValue(),
937              download_util::SMALL);
938        }
939      }
940    }
941
942    // Draw the icon image.
943    int mirrored_x = GetMirroredXWithWidthInView(
944        download_util::kSmallProgressIconOffset, icon->width());
945    if (IsEnabled()) {
946      canvas->DrawBitmapInt(*icon, mirrored_x,
947                            download_util::kSmallProgressIconOffset);
948    } else {
949      // Use an alpha to make the image look disabled.
950      SkPaint paint;
951      paint.setAlpha(120);
952      canvas->DrawBitmapInt(*icon, mirrored_x,
953                            download_util::kSmallProgressIconOffset, paint);
954    }
955  }
956}
957
958void DownloadItemView::OpenDownload() {
959  // We're interested in how long it takes users to open downloads.  If they
960  // open downloads super quickly, we should be concerned about clickjacking.
961  UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
962                           base::Time::Now() - creation_time_);
963  download_->OpenDownload();
964  UpdateAccessibleName();
965}
966
967void DownloadItemView::LoadIcon() {
968  IconManager* im = g_browser_process->icon_manager();
969  im->LoadIcon(download_->GetUserVerifiedFilePath(),
970               IconLoader::SMALL, &icon_consumer_,
971               NewCallback(this, &DownloadItemView::OnExtractIconComplete));
972}
973
974// Load an icon for the file type we're downloading, and animate any in progress
975// download state.
976void DownloadItemView::PaintBitmaps(gfx::Canvas* canvas,
977                                    const SkBitmap* top_bitmap,
978                                    const SkBitmap* center_bitmap,
979                                    const SkBitmap* bottom_bitmap,
980                                    int x, int y, int height, int width) {
981  int middle_height = height - top_bitmap->height() - bottom_bitmap->height();
982  // Draw the top.
983  canvas->DrawBitmapInt(*top_bitmap,
984                        0, 0, top_bitmap->width(), top_bitmap->height(),
985                        x, y, width, top_bitmap->height(), false);
986  y += top_bitmap->height();
987  // Draw the center.
988  canvas->DrawBitmapInt(*center_bitmap,
989                        0, 0, center_bitmap->width(), center_bitmap->height(),
990                        x, y, width, middle_height, false);
991  y += middle_height;
992  // Draw the bottom.
993  canvas->DrawBitmapInt(*bottom_bitmap,
994                        0, 0, bottom_bitmap->width(), bottom_bitmap->height(),
995                        x, y, width, bottom_bitmap->height(), false);
996}
997
998void DownloadItemView::SetState(State body_state, State drop_down_state) {
999  if (body_state_ == body_state && drop_down_state_ == drop_down_state)
1000    return;
1001
1002  body_state_ = body_state;
1003  drop_down_state_ = drop_down_state;
1004  SchedulePaint();
1005}
1006
1007void DownloadItemView::ClearDangerousMode() {
1008  DCHECK(download_->safety_state() == DownloadItem::DANGEROUS_BUT_VALIDATED &&
1009         body_state_ == DANGEROUS && drop_down_state_ == DANGEROUS);
1010
1011  body_state_ = NORMAL;
1012  drop_down_state_ = NORMAL;
1013
1014  // Remove the views used by the dangerous mode.
1015  RemoveChildView(save_button_);
1016  delete save_button_;
1017  save_button_ = NULL;
1018  RemoveChildView(discard_button_);
1019  delete discard_button_;
1020  discard_button_ = NULL;
1021  RemoveChildView(dangerous_download_label_);
1022  delete dangerous_download_label_;
1023  dangerous_download_label_ = NULL;
1024
1025  // Set the accessible name back to the status and filename instead of the
1026  // download warning.
1027  UpdateAccessibleName();
1028
1029  // We need to load the icon now that the download_ has the real path.
1030  LoadIcon();
1031  tooltip_text_ =
1032      UTF16ToWide(download_->GetFileNameToReportUser().LossyDisplayName());
1033
1034  // Force the shelf to layout again as our size has changed.
1035  parent_->Layout();
1036  parent_->SchedulePaint();
1037}
1038
1039gfx::Size DownloadItemView::GetButtonSize() {
1040  DCHECK(save_button_ && discard_button_);
1041  gfx::Size size;
1042
1043  // We cache the size when successfully retrieved, not for performance reasons
1044  // but because if this DownloadItemView is being animated while the tab is
1045  // not showing, the native buttons are not parented and their preferred size
1046  // is 0, messing-up the layout.
1047  if (cached_button_size_.width() != 0)
1048    return cached_button_size_;
1049
1050  size = save_button_->GetMinimumSize();
1051  gfx::Size discard_size = discard_button_->GetMinimumSize();
1052
1053  size.SetSize(std::max(size.width(), discard_size.width()),
1054               std::max(size.height(), discard_size.height()));
1055
1056  if (size.width() != 0)
1057    cached_button_size_ = size;
1058
1059  return size;
1060}
1061
1062// This method computes the minimum width of the label for displaying its text
1063// on 2 lines.  It just breaks the string in 2 lines on the spaces and keeps the
1064// configuration with minimum width.
1065void DownloadItemView::SizeLabelToMinWidth() {
1066  if (dangerous_download_label_sized_)
1067    return;
1068
1069  std::wstring text = dangerous_download_label_->GetText();
1070  TrimWhitespace(text, TRIM_ALL, &text);
1071  DCHECK_EQ(std::wstring::npos, text.find(L"\n"));
1072
1073  // Make the label big so that GetPreferredSize() is not constrained by the
1074  // current width.
1075  dangerous_download_label_->SetBounds(0, 0, 1000, 1000);
1076
1077  gfx::Size size;
1078  int min_width = -1;
1079  string16 text16 = WideToUTF16(text);
1080  // Using BREAK_WORD can work in most cases, but it can also break
1081  // lines where it should not. Using BREAK_LINE is safer although
1082  // slower for Chinese/Japanese. This is not perf-critical at all, though.
1083  base::BreakIterator iter(&text16, base::BreakIterator::BREAK_LINE);
1084  bool status = iter.Init();
1085  DCHECK(status);
1086
1087  string16 current_text = text16;
1088  string16 prev_text = text16;
1089  while (iter.Advance()) {
1090    size_t pos = iter.pos();
1091    if (pos >= text16.length())
1092      break;
1093    // This can be a low surrogate codepoint, but u_isUWhiteSpace will
1094    // return false and inserting a new line after a surrogate pair
1095    // is perfectly ok.
1096    char16 line_end_char = text16[pos - 1];
1097    if (u_isUWhiteSpace(line_end_char))
1098      current_text.replace(pos - 1, 1, 1, char16('\n'));
1099    else
1100      current_text.insert(pos, 1, char16('\n'));
1101    dangerous_download_label_->SetText(UTF16ToWide(current_text));
1102    size = dangerous_download_label_->GetPreferredSize();
1103
1104    if (min_width == -1)
1105      min_width = size.width();
1106
1107    // If the width is growing again, it means we passed the optimal width spot.
1108    if (size.width() > min_width) {
1109      dangerous_download_label_->SetText(UTF16ToWide(prev_text));
1110      break;
1111    } else {
1112      min_width = size.width();
1113    }
1114
1115    // Restore the string.
1116    prev_text = current_text;
1117    current_text = text16;
1118  }
1119
1120  // If we have a line with no line breaking opportunity (which is very
1121  // unlikely), we won't cut it.
1122  if (min_width == -1)
1123    size = dangerous_download_label_->GetPreferredSize();
1124
1125  dangerous_download_label_->SetBounds(0, 0, size.width(), size.height());
1126  dangerous_download_label_sized_ = true;
1127}
1128
1129void DownloadItemView::Reenable() {
1130  disabled_while_opening_ = false;
1131  SetEnabled(true);  // Triggers a repaint.
1132}
1133
1134bool DownloadItemView::InDropDownButtonXCoordinateRange(int x) {
1135  if (x > drop_down_x_left_ && x < drop_down_x_right_)
1136    return true;
1137  return false;
1138}
1139
1140void DownloadItemView::UpdateAccessibleName() {
1141  string16 new_name;
1142  if (download_->safety_state() == DownloadItem::DANGEROUS) {
1143    new_name = WideToUTF16Hack(dangerous_download_label_->GetText());
1144  } else {
1145    new_name = WideToUTF16Hack(status_text_) + char16(' ') +
1146        download_->GetFileNameToReportUser().LossyDisplayName();
1147  }
1148
1149  // If the name has changed, notify assistive technology that the name
1150  // has changed so they can announce it immediately.
1151  if (new_name != accessible_name_) {
1152    accessible_name_ = new_name;
1153    if (GetWidget()) {
1154      GetWidget()->NotifyAccessibilityEvent(
1155          this, ui::AccessibilityTypes::EVENT_NAME_CHANGED, true);
1156    }
1157  }
1158}
1159