download_item_view.cc revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
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/views/download/download_item_view.h"
6
7#include <algorithm>
8#include <vector>
9
10#include "base/bind.h"
11#include "base/callback.h"
12#include "base/files/file_path.h"
13#include "base/i18n/break_iterator.h"
14#include "base/i18n/rtl.h"
15#include "base/metrics/histogram.h"
16#include "base/strings/string_util.h"
17#include "base/strings/stringprintf.h"
18#include "base/strings/sys_string_conversions.h"
19#include "base/strings/utf_string_conversions.h"
20#include "chrome/browser/browser_process.h"
21#include "chrome/browser/download/chrome_download_manager_delegate.h"
22#include "chrome/browser/download/download_item_model.h"
23#include "chrome/browser/download/drag_download_item.h"
24#include "chrome/browser/safe_browsing/download_feedback_service.h"
25#include "chrome/browser/safe_browsing/download_protection_service.h"
26#include "chrome/browser/safe_browsing/safe_browsing_service.h"
27#include "chrome/browser/themes/theme_properties.h"
28#include "chrome/browser/ui/views/download/download_shelf_context_menu_view.h"
29#include "chrome/browser/ui/views/download/download_shelf_view.h"
30#include "content/public/browser/download_danger_type.h"
31#include "grit/generated_resources.h"
32#include "grit/theme_resources.h"
33#include "third_party/icu/source/common/unicode/uchar.h"
34#include "ui/base/accessibility/accessible_view_state.h"
35#include "ui/base/l10n/l10n_util.h"
36#include "ui/base/resource/resource_bundle.h"
37#include "ui/base/theme_provider.h"
38#include "ui/events/event.h"
39#include "ui/gfx/animation/slide_animation.h"
40#include "ui/gfx/canvas.h"
41#include "ui/gfx/color_utils.h"
42#include "ui/gfx/image/image.h"
43#include "ui/gfx/text_elider.h"
44#include "ui/views/controls/button/label_button.h"
45#include "ui/views/controls/label.h"
46#include "ui/views/widget/root_view.h"
47#include "ui/views/widget/widget.h"
48
49// TODO(paulg): These may need to be adjusted when download progress
50//              animation is added, and also possibly to take into account
51//              different screen resolutions.
52static const int kTextWidth = 140;            // Pixels
53static const int kDangerousTextWidth = 200;   // Pixels
54static const int kHorizontalTextPadding = 2;  // Pixels
55static const int kVerticalPadding = 3;        // Pixels
56static const int kVerticalTextSpacer = 2;     // Pixels
57static const int kVerticalTextPadding = 2;    // Pixels
58static const int kTooltipMaxWidth = 800;      // Pixels
59
60// We add some padding before the left image so that the progress animation icon
61// hides the corners of the left image.
62static const int kLeftPadding = 0;  // Pixels.
63
64// The space between the Save and Discard buttons when prompting for a dangerous
65// download.
66static const int kButtonPadding = 5;  // Pixels.
67
68// The space on the left and right side of the dangerous download label.
69static const int kLabelPadding = 4;  // Pixels.
70
71static const SkColor kFileNameDisabledColor = SkColorSetRGB(171, 192, 212);
72
73// How long the 'download complete' animation should last for.
74static const int kCompleteAnimationDurationMs = 2500;
75
76// How long the 'download interrupted' animation should last for.
77static const int kInterruptedAnimationDurationMs = 2500;
78
79// How long we keep the item disabled after the user clicked it to open the
80// downloaded item.
81static const int kDisabledOnOpenDuration = 3000;
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.
86static const double kDownloadItemLuminanceMod = 0.8;
87
88using content::DownloadItem;
89
90DownloadItemView::DownloadItemView(DownloadItem* download_item,
91    DownloadShelfView* parent)
92  : warning_icon_(NULL),
93    shelf_(parent),
94    status_text_(l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING)),
95    body_state_(NORMAL),
96    drop_down_state_(NORMAL),
97    mode_(NORMAL_MODE),
98    progress_angle_(DownloadShelf::kStartAngleDegrees),
99    drop_down_pressed_(false),
100    dragging_(false),
101    starting_drag_(false),
102    model_(download_item),
103    save_button_(NULL),
104    discard_button_(NULL),
105    dangerous_download_label_(NULL),
106    dangerous_download_label_sized_(false),
107    disabled_while_opening_(false),
108    creation_time_(base::Time::Now()),
109    weak_ptr_factory_(this) {
110  DCHECK(download());
111  download()->AddObserver(this);
112  set_context_menu_controller(this);
113
114  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
115
116  BodyImageSet normal_body_image_set = {
117    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP),
118    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE),
119    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM),
120    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP),
121    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE),
122    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM),
123    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP),
124    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE),
125    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM)
126  };
127  normal_body_image_set_ = normal_body_image_set;
128
129  DropDownImageSet normal_drop_down_image_set = {
130    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP),
131    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE),
132    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM)
133  };
134  normal_drop_down_image_set_ = normal_drop_down_image_set;
135
136  BodyImageSet hot_body_image_set = {
137    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP_H),
138    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_H),
139    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_H),
140    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP_H),
141    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_H),
142    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_H),
143    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_H),
144    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_H),
145    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_H)
146  };
147  hot_body_image_set_ = hot_body_image_set;
148
149  DropDownImageSet hot_drop_down_image_set = {
150    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP_H),
151    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_H),
152    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_H)
153  };
154  hot_drop_down_image_set_ = hot_drop_down_image_set;
155
156  BodyImageSet pushed_body_image_set = {
157    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP_P),
158    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_P),
159    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_P),
160    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP_P),
161    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_P),
162    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_P),
163    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_P),
164    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_P),
165    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_P)
166  };
167  pushed_body_image_set_ = pushed_body_image_set;
168
169  DropDownImageSet pushed_drop_down_image_set = {
170    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP_P),
171    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_P),
172    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_P)
173  };
174  pushed_drop_down_image_set_ = pushed_drop_down_image_set;
175
176  BodyImageSet dangerous_mode_body_image_set = {
177    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP),
178    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE),
179    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM),
180    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP),
181    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE),
182    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM),
183    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_NO_DD),
184    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_NO_DD),
185    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_NO_DD)
186  };
187  dangerous_mode_body_image_set_ = dangerous_mode_body_image_set;
188
189  malicious_mode_body_image_set_ = normal_body_image_set;
190
191  LoadIcon();
192
193  font_list_ = rb.GetFontList(ui::ResourceBundle::BaseFont);
194  box_height_ = std::max<int>(2 * kVerticalPadding + font_list_.GetHeight() +
195                                  kVerticalTextPadding + font_list_.GetHeight(),
196                              2 * kVerticalPadding +
197                                  normal_body_image_set_.top_left->height() +
198                                  normal_body_image_set_.bottom_left->height());
199
200  if (DownloadShelf::kSmallProgressIconSize > box_height_)
201    box_y_ = (DownloadShelf::kSmallProgressIconSize - box_height_) / 2;
202  else
203    box_y_ = 0;
204
205  body_hover_animation_.reset(new gfx::SlideAnimation(this));
206  drop_hover_animation_.reset(new gfx::SlideAnimation(this));
207
208  set_accessibility_focusable(true);
209
210  OnDownloadUpdated(download());
211  UpdateDropDownButtonPosition();
212}
213
214DownloadItemView::~DownloadItemView() {
215  StopDownloadProgress();
216  download()->RemoveObserver(this);
217}
218
219// Progress animation handlers.
220
221void DownloadItemView::UpdateDownloadProgress() {
222  progress_angle_ =
223      (progress_angle_ + DownloadShelf::kUnknownIncrementDegrees) %
224      DownloadShelf::kMaxDegrees;
225  SchedulePaint();
226}
227
228void DownloadItemView::StartDownloadProgress() {
229  if (progress_timer_.IsRunning())
230    return;
231  progress_timer_.Start(FROM_HERE,
232      base::TimeDelta::FromMilliseconds(DownloadShelf::kProgressRateMs), this,
233      &DownloadItemView::UpdateDownloadProgress);
234}
235
236void DownloadItemView::StopDownloadProgress() {
237  progress_timer_.Stop();
238}
239
240void DownloadItemView::OnExtractIconComplete(gfx::Image* icon_bitmap) {
241  if (icon_bitmap)
242    shelf_->SchedulePaint();
243}
244
245// DownloadObserver interface.
246
247// Update the progress graphic on the icon and our text status label
248// to reflect our current bytes downloaded, time remaining.
249void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) {
250  DCHECK_EQ(download(), download_item);
251
252  if (IsShowingWarningDialog() && !model_.IsDangerous()) {
253    // We have been approved.
254    ClearWarningDialog();
255  } else if (!IsShowingWarningDialog() && model_.IsDangerous()) {
256    ShowWarningDialog();
257    // Force the shelf to layout again as our size has changed.
258    shelf_->Layout();
259    SchedulePaint();
260  } else {
261    string16 status_text = model_.GetStatusText();
262    switch (download()->GetState()) {
263      case DownloadItem::IN_PROGRESS:
264        download()->IsPaused() ?
265            StopDownloadProgress() : StartDownloadProgress();
266        LoadIconIfItemPathChanged();
267        break;
268      case DownloadItem::INTERRUPTED:
269        StopDownloadProgress();
270        complete_animation_.reset(new gfx::SlideAnimation(this));
271        complete_animation_->SetSlideDuration(kInterruptedAnimationDurationMs);
272        complete_animation_->SetTweenType(gfx::Tween::LINEAR);
273        complete_animation_->Show();
274        SchedulePaint();
275        LoadIcon();
276        break;
277      case DownloadItem::COMPLETE:
278        if (model_.ShouldRemoveFromShelfWhenComplete()) {
279          shelf_->RemoveDownloadView(this);  // This will delete us!
280          return;
281        }
282        StopDownloadProgress();
283        complete_animation_.reset(new gfx::SlideAnimation(this));
284        complete_animation_->SetSlideDuration(kCompleteAnimationDurationMs);
285        complete_animation_->SetTweenType(gfx::Tween::LINEAR);
286        complete_animation_->Show();
287        SchedulePaint();
288        LoadIcon();
289        break;
290      case DownloadItem::CANCELLED:
291        StopDownloadProgress();
292        if (complete_animation_)
293          complete_animation_->Stop();
294        LoadIcon();
295        break;
296      default:
297        NOTREACHED();
298    }
299    status_text_ = status_text;
300  }
301
302  string16 new_tip = model_.GetTooltipText(font_list_, kTooltipMaxWidth);
303  if (new_tip != tooltip_text_) {
304    tooltip_text_ = new_tip;
305    TooltipTextChanged();
306  }
307
308  UpdateAccessibleName();
309
310  // We use the parent's (DownloadShelfView's) SchedulePaint, since there
311  // are spaces between each DownloadItemView that the parent is responsible
312  // for painting.
313  shelf_->SchedulePaint();
314}
315
316void DownloadItemView::OnDownloadDestroyed(DownloadItem* download) {
317  shelf_->RemoveDownloadView(this);  // This will delete us!
318}
319
320void DownloadItemView::OnDownloadOpened(DownloadItem* download) {
321  disabled_while_opening_ = true;
322  SetEnabled(false);
323  base::MessageLoop::current()->PostDelayedTask(
324      FROM_HERE,
325      base::Bind(&DownloadItemView::Reenable, weak_ptr_factory_.GetWeakPtr()),
326      base::TimeDelta::FromMilliseconds(kDisabledOnOpenDuration));
327
328  // Notify our parent.
329  shelf_->OpenedDownload(this);
330}
331
332// View overrides
333
334// In dangerous mode we have to layout our buttons.
335void DownloadItemView::Layout() {
336  if (IsShowingWarningDialog()) {
337    BodyImageSet* body_image_set =
338        (mode_ == DANGEROUS_MODE) ? &dangerous_mode_body_image_set_ :
339            &malicious_mode_body_image_set_;
340    int x = kLeftPadding + body_image_set->top_left->width() +
341        warning_icon_->width() + kLabelPadding;
342    int y = (height() - dangerous_download_label_->height()) / 2;
343    dangerous_download_label_->SetBounds(x, y,
344                                         dangerous_download_label_->width(),
345                                         dangerous_download_label_->height());
346    gfx::Size button_size = GetButtonSize();
347    x += dangerous_download_label_->width() + kLabelPadding;
348    y = (height() - button_size.height()) / 2;
349    if (save_button_) {
350      save_button_->SetBounds(x, y, button_size.width(), button_size.height());
351      x += button_size.width() + kButtonPadding;
352    }
353    discard_button_->SetBounds(x, y, button_size.width(), button_size.height());
354    UpdateColorsFromTheme();
355  }
356}
357
358gfx::Size DownloadItemView::GetPreferredSize() {
359  int width, height;
360
361  // First, we set the height to the height of two rows or text plus margins.
362  height = 2 * kVerticalPadding + 2 * font_list_.GetHeight() +
363      kVerticalTextPadding;
364  // Then we increase the size if the progress icon doesn't fit.
365  height = std::max<int>(height, DownloadShelf::kSmallProgressIconSize);
366
367  if (IsShowingWarningDialog()) {
368    BodyImageSet* body_image_set =
369        (mode_ == DANGEROUS_MODE) ? &dangerous_mode_body_image_set_ :
370            &malicious_mode_body_image_set_;
371    width = kLeftPadding + body_image_set->top_left->width();
372    width += warning_icon_->width() + kLabelPadding;
373    width += dangerous_download_label_->width() + kLabelPadding;
374    gfx::Size button_size = GetButtonSize();
375    // Make sure the button fits.
376    height = std::max<int>(height, 2 * kVerticalPadding + button_size.height());
377    // Then we make sure the warning icon fits.
378    height = std::max<int>(height, 2 * kVerticalPadding +
379                                   warning_icon_->height());
380    if (save_button_)
381      width += button_size.width() + kButtonPadding;
382    width += button_size.width();
383    width += body_image_set->top_right->width();
384    if (mode_ == MALICIOUS_MODE)
385      width += normal_drop_down_image_set_.top->width();
386  } else {
387    width = kLeftPadding + normal_body_image_set_.top_left->width();
388    width += DownloadShelf::kSmallProgressIconSize;
389    width += kTextWidth;
390    width += normal_body_image_set_.top_right->width();
391    width += normal_drop_down_image_set_.top->width();
392  }
393  return gfx::Size(width, height);
394}
395
396// Handle a mouse click and open the context menu if the mouse is
397// over the drop-down region.
398bool DownloadItemView::OnMousePressed(const ui::MouseEvent& event) {
399  HandlePressEvent(event, event.IsOnlyLeftMouseButton());
400  return true;
401}
402
403// Handle drag (file copy) operations.
404bool DownloadItemView::OnMouseDragged(const ui::MouseEvent& event) {
405  // Mouse should not activate us in dangerous mode.
406  if (IsShowingWarningDialog())
407    return true;
408
409  if (!starting_drag_) {
410    starting_drag_ = true;
411    drag_start_point_ = event.location();
412  }
413  if (dragging_) {
414    if (download()->GetState() == DownloadItem::COMPLETE) {
415      IconManager* im = g_browser_process->icon_manager();
416      gfx::Image* icon = im->LookupIconFromFilepath(
417          download()->GetTargetFilePath(), IconLoader::SMALL);
418      if (icon) {
419        views::Widget* widget = GetWidget();
420        DragDownloadItem(
421            download(), icon, widget ? widget->GetNativeView() : NULL);
422      }
423    }
424  } else if (ExceededDragThreshold(event.location() - drag_start_point_)) {
425    dragging_ = true;
426  }
427  return true;
428}
429
430void DownloadItemView::OnMouseReleased(const ui::MouseEvent& event) {
431  HandleClickEvent(event, event.IsOnlyLeftMouseButton());
432}
433
434void DownloadItemView::OnMouseCaptureLost() {
435  // Mouse should not activate us in dangerous mode.
436  if (mode_ == DANGEROUS_MODE)
437    return;
438
439  if (dragging_) {
440    // Starting a drag results in a MouseCaptureLost.
441    dragging_ = false;
442    starting_drag_ = false;
443  }
444  SetState(NORMAL, NORMAL);
445}
446
447void DownloadItemView::OnMouseMoved(const ui::MouseEvent& event) {
448  // Mouse should not activate us in dangerous mode.
449  if (mode_ == DANGEROUS_MODE)
450    return;
451
452  bool on_body = !InDropDownButtonXCoordinateRange(event.x());
453  SetState(on_body ? HOT : NORMAL, on_body ? NORMAL : HOT);
454}
455
456void DownloadItemView::OnMouseExited(const ui::MouseEvent& event) {
457  // Mouse should not activate us in dangerous mode.
458  if (mode_ == DANGEROUS_MODE)
459    return;
460
461  SetState(NORMAL, drop_down_pressed_ ? PUSHED : NORMAL);
462}
463
464bool DownloadItemView::OnKeyPressed(const ui::KeyEvent& event) {
465  // Key press should not activate us in dangerous mode.
466  if (IsShowingWarningDialog())
467    return true;
468
469  if (event.key_code() == ui::VKEY_SPACE ||
470      event.key_code() == ui::VKEY_RETURN) {
471    OpenDownload();
472    return true;
473  }
474  return false;
475}
476
477bool DownloadItemView::GetTooltipText(const gfx::Point& p,
478                                      string16* tooltip) const {
479  if (IsShowingWarningDialog()) {
480    tooltip->clear();
481    return false;
482  }
483
484  tooltip->assign(tooltip_text_);
485
486  return true;
487}
488
489void DownloadItemView::GetAccessibleState(ui::AccessibleViewState* state) {
490  state->name = accessible_name_;
491  state->role = ui::AccessibilityTypes::ROLE_PUSHBUTTON;
492  if (model_.IsDangerous()) {
493    state->state = ui::AccessibilityTypes::STATE_UNAVAILABLE;
494  } else {
495    state->state = ui::AccessibilityTypes::STATE_HASPOPUP;
496  }
497}
498
499void DownloadItemView::OnThemeChanged() {
500  UpdateColorsFromTheme();
501}
502
503void DownloadItemView::OnGestureEvent(ui::GestureEvent* event) {
504  if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
505    HandlePressEvent(*event, true);
506    event->SetHandled();
507    return;
508  }
509
510  if (event->type() == ui::ET_GESTURE_TAP) {
511    HandleClickEvent(*event, true);
512    event->SetHandled();
513    return;
514  }
515
516  SetState(NORMAL, NORMAL);
517  views::View::OnGestureEvent(event);
518}
519
520void DownloadItemView::ShowContextMenuForView(View* source,
521                                              const gfx::Point& point,
522                                              ui::MenuSourceType source_type) {
523  // |point| is in screen coordinates. So convert it to local coordinates first.
524  gfx::Point local_point = point;
525  ConvertPointFromScreen(this, &local_point);
526  ShowContextMenuImpl(local_point, source_type);
527}
528
529void DownloadItemView::ButtonPressed(
530    views::Button* sender, const ui::Event& event) {
531  if (sender == discard_button_) {
532    if (model_.ShouldAllowDownloadFeedback() && BeginDownloadFeedback())
533      return;
534    UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
535                             base::Time::Now() - creation_time_);
536    download()->Remove();
537    // WARNING: we are deleted at this point.  Don't access 'this'.
538  } else if (save_button_ && sender == save_button_) {
539    // The user has confirmed a dangerous download.  We'd record how quickly the
540    // user did this to detect whether we're being clickjacked.
541    UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
542                             base::Time::Now() - creation_time_);
543    // This will change the state and notify us.
544    download()->ValidateDangerousDownload();
545  }
546}
547
548void DownloadItemView::AnimationProgressed(const gfx::Animation* animation) {
549  // We don't care if what animation (body button/drop button/complete),
550  // is calling back, as they all have to go through the same paint call.
551  SchedulePaint();
552}
553
554// The DownloadItemView can be in three major modes (NORMAL_MODE, DANGEROUS_MODE
555// and MALICIOUS_MODE).
556//
557// NORMAL_MODE: We are displaying an in-progress or completed download.
558// .-------------------------------+-.
559// | [icon] Filename               |v|
560// | [    ] Status                 | |
561// `-------------------------------+-'
562//  |  |                            \_ Drop down button. Invokes menu. Responds
563//  |  |                               to mouse. (NORMAL, HOT or PUSHED).
564//  |   \_ Icon is overlaid on top of in-progress animation.
565//   \_ Both the body and the drop down button respond to mouse hover and can be
566//      pushed (NORMAL, HOT or PUSHED).
567//
568// DANGEROUS_MODE: The file could be potentially dangerous.
569// .-------------------------------------------------------.
570// | [ ! ] [This type of file can  ]  [ Keep ] [ Discard ] |
571// | [   ] [destroy your computer..]  [      ] [         ] |
572// `-------------------------------------------------------'
573//  |  |    |                          |                 \_ No drop down button.
574//  |  |    |                           \_ Buttons are views::LabelButtons.
575//  |  |     \_ Text is in a label (dangerous_download_label_)
576//  |   \_ Warning icon.  No progress animation.
577//   \_ Body is static.  Doesn't respond to mouse hover or press. (NORMAL only)
578//
579// MALICIOUS_MODE: The file is known malware.
580// .---------------------------------------------+-.
581// | [ - ] [This file is malicious.] [ Discard ] |v|
582// | [   ] [                       ] [         ] | |-.
583// `---------------------------------------------+-' |
584//  |  |    |                         |            Drop down button. Responds to
585//  |  |    |                         |            mouse.(NORMAL, HOT or PUSHED)
586//  |  |    |                          \_ Button is a views::LabelButton.
587//  |  |     \_ Text is in a label (dangerous_download_label_)
588//  |   \_ Warning icon.  No progress animation.
589//   \_ Body is static.  Doesn't respond to mouse hover or press. (NORMAL only)
590//
591void DownloadItemView::OnPaint(gfx::Canvas* canvas) {
592  BodyImageSet* body_image_set = NULL;
593  switch (mode_) {
594    case NORMAL_MODE:
595      if (body_state_ == PUSHED)
596        body_image_set = &pushed_body_image_set_;
597      else                      // NORMAL or HOT
598        body_image_set = &normal_body_image_set_;
599      break;
600    case DANGEROUS_MODE:
601      body_image_set = &dangerous_mode_body_image_set_;
602      break;
603    case MALICIOUS_MODE:
604      body_image_set = &malicious_mode_body_image_set_;
605      break;
606    default:
607      NOTREACHED();
608  }
609
610  DropDownImageSet* drop_down_image_set = NULL;
611  switch (mode_) {
612    case NORMAL_MODE:
613    case MALICIOUS_MODE:
614      if (drop_down_state_ == PUSHED)
615        drop_down_image_set = &pushed_drop_down_image_set_;
616      else                        // NORMAL or HOT
617        drop_down_image_set = &normal_drop_down_image_set_;
618      break;
619    case DANGEROUS_MODE:
620      // We don't use a drop down button for mode_ == DANGEROUS_MODE.  So we let
621      // drop_down_image_set == NULL.
622      break;
623    default:
624      NOTREACHED();
625  }
626
627  int center_width = width() - kLeftPadding -
628                     body_image_set->left->width() -
629                     body_image_set->right->width() -
630                     (drop_down_image_set ?
631                        normal_drop_down_image_set_.center->width() :
632                        0);
633
634  // May be caused by animation.
635  if (center_width <= 0)
636    return;
637
638  // Draw status before button image to effectively lighten text.  No status for
639  // warning dialogs.
640  if (!IsShowingWarningDialog()) {
641    if (!status_text_.empty()) {
642      int mirrored_x = GetMirroredXWithWidthInView(
643          DownloadShelf::kSmallProgressIconSize, kTextWidth);
644      // Add font_list_.height() to compensate for title, which is drawn later.
645      int y = box_y_ + kVerticalPadding + font_list_.GetHeight() +
646              kVerticalTextPadding;
647      SkColor file_name_color = GetThemeProvider()->GetColor(
648          ThemeProperties::COLOR_BOOKMARK_TEXT);
649      // If text is light-on-dark, lightening it alone will do nothing.
650      // Therefore we mute luminance a wee bit before drawing in this case.
651      if (color_utils::RelativeLuminance(file_name_color) > 0.5)
652          file_name_color = SkColorSetRGB(
653              static_cast<int>(kDownloadItemLuminanceMod *
654                               SkColorGetR(file_name_color)),
655              static_cast<int>(kDownloadItemLuminanceMod *
656                               SkColorGetG(file_name_color)),
657              static_cast<int>(kDownloadItemLuminanceMod *
658                               SkColorGetB(file_name_color)));
659      canvas->DrawStringRect(status_text_, font_list_, file_name_color,
660                             gfx::Rect(mirrored_x, y, kTextWidth,
661                                       font_list_.GetHeight()));
662    }
663  }
664
665  // Paint the background images.
666  int x = kLeftPadding;
667  canvas->Save();
668  if (base::i18n::IsRTL()) {
669    // Since we do not have the mirrored images for
670    // (hot_)body_image_set->top_left, (hot_)body_image_set->left,
671    // (hot_)body_image_set->bottom_left, and drop_down_image_set,
672    // for RTL UI, we flip the canvas to draw those images mirrored.
673    // Consequently, we do not need to mirror the x-axis of those images.
674    canvas->Translate(gfx::Vector2d(width(), 0));
675    canvas->Scale(-1, 1);
676  }
677  PaintImages(canvas,
678              body_image_set->top_left, body_image_set->left,
679              body_image_set->bottom_left,
680              x, box_y_, box_height_, body_image_set->top_left->width());
681  x += body_image_set->top_left->width();
682  PaintImages(canvas,
683              body_image_set->top, body_image_set->center,
684              body_image_set->bottom,
685              x, box_y_, box_height_, center_width);
686  x += center_width;
687  PaintImages(canvas,
688              body_image_set->top_right, body_image_set->right,
689              body_image_set->bottom_right,
690              x, box_y_, box_height_, body_image_set->top_right->width());
691
692  // Overlay our body hot state. Warning dialogs don't display body a hot state.
693  if (!IsShowingWarningDialog() &&
694      body_hover_animation_->GetCurrentValue() > 0) {
695    canvas->SaveLayerAlpha(
696        static_cast<int>(body_hover_animation_->GetCurrentValue() * 255));
697
698    int x = kLeftPadding;
699    PaintImages(canvas,
700                hot_body_image_set_.top_left, hot_body_image_set_.left,
701                hot_body_image_set_.bottom_left,
702                x, box_y_, box_height_, hot_body_image_set_.top_left->width());
703    x += body_image_set->top_left->width();
704    PaintImages(canvas,
705                hot_body_image_set_.top, hot_body_image_set_.center,
706                hot_body_image_set_.bottom,
707                x, box_y_, box_height_, center_width);
708    x += center_width;
709    PaintImages(canvas,
710                hot_body_image_set_.top_right, hot_body_image_set_.right,
711                hot_body_image_set_.bottom_right,
712                x, box_y_, box_height_,
713                hot_body_image_set_.top_right->width());
714    canvas->Restore();
715  }
716
717  x += body_image_set->top_right->width();
718
719  // Paint the drop-down.
720  if (drop_down_image_set) {
721    PaintImages(canvas,
722                drop_down_image_set->top, drop_down_image_set->center,
723                drop_down_image_set->bottom,
724                x, box_y_, box_height_, drop_down_image_set->top->width());
725
726    // Overlay our drop-down hot state.
727    if (drop_hover_animation_->GetCurrentValue() > 0) {
728      canvas->SaveLayerAlpha(
729          static_cast<int>(drop_hover_animation_->GetCurrentValue() * 255));
730
731      PaintImages(canvas,
732                  drop_down_image_set->top, drop_down_image_set->center,
733                  drop_down_image_set->bottom,
734                  x, box_y_, box_height_, drop_down_image_set->top->width());
735
736      canvas->Restore();
737    }
738  }
739
740  // Restore the canvas to avoid file name etc. text are drawn flipped.
741  // Consequently, the x-axis of following canvas->DrawXXX() method should be
742  // mirrored so the text and images are down in the right positions.
743  canvas->Restore();
744
745  // Print the text, left aligned and always print the file extension.
746  // Last value of x was the end of the right image, just before the button.
747  // Note that in dangerous mode we use a label (as the text is multi-line).
748  if (!IsShowingWarningDialog()) {
749    string16 filename;
750    if (!disabled_while_opening_) {
751      filename = gfx::ElideFilename(download()->GetFileNameToReportUser(),
752                                   font_list_, kTextWidth);
753    } else {
754      // First, Calculate the download status opening string width.
755      string16 status_string =
756          l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING, string16());
757      int status_string_width = font_list_.GetStringWidth(status_string);
758      // Then, elide the file name.
759      string16 filename_string =
760          gfx::ElideFilename(download()->GetFileNameToReportUser(), font_list_,
761                            kTextWidth - status_string_width);
762      // Last, concat the whole string.
763      filename = l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
764                                            filename_string);
765    }
766
767    int mirrored_x = GetMirroredXWithWidthInView(
768        DownloadShelf::kSmallProgressIconSize, kTextWidth);
769    SkColor file_name_color = GetThemeProvider()->GetColor(
770        ThemeProperties::COLOR_BOOKMARK_TEXT);
771    int y =
772        box_y_ + (status_text_.empty() ?
773            ((box_height_ - font_list_.GetHeight()) / 2) : kVerticalPadding);
774
775    // Draw the file's name.
776    canvas->DrawStringRect(
777        filename, font_list_,
778        enabled() ? file_name_color : kFileNameDisabledColor,
779        gfx::Rect(mirrored_x, y, kTextWidth, font_list_.GetHeight()));
780  }
781
782  // Load the icon.
783  IconManager* im = g_browser_process->icon_manager();
784  gfx::Image* image = im->LookupIconFromFilepath(
785      download()->GetTargetFilePath(), IconLoader::SMALL);
786  const gfx::ImageSkia* icon = NULL;
787  if (IsShowingWarningDialog())
788    icon = warning_icon_;
789  else if (image)
790    icon = image->ToImageSkia();
791
792  // We count on the fact that the icon manager will cache the icons and if one
793  // is available, it will be cached here. We *don't* want to request the icon
794  // to be loaded here, since this will also get called if the icon can't be
795  // loaded, in which case LookupIcon will always be NULL. The loading will be
796  // triggered only when we think the status might change.
797  if (icon) {
798    if (!IsShowingWarningDialog()) {
799      DownloadItem::DownloadState state = download()->GetState();
800      if (state == DownloadItem::IN_PROGRESS) {
801        DownloadShelf::PaintDownloadProgress(canvas,
802                                             this,
803                                             0,
804                                             0,
805                                             progress_angle_,
806                                             model_.PercentComplete(),
807                                             DownloadShelf::SMALL);
808      } else if (complete_animation_.get() &&
809                 complete_animation_->is_animating()) {
810        if (state == DownloadItem::INTERRUPTED) {
811          DownloadShelf::PaintDownloadInterrupted(
812              canvas,
813              this,
814              0,
815              0,
816              complete_animation_->GetCurrentValue(),
817              DownloadShelf::SMALL);
818        } else {
819          DCHECK_EQ(DownloadItem::COMPLETE, state);
820          DownloadShelf::PaintDownloadComplete(
821              canvas,
822              this,
823              0,
824              0,
825              complete_animation_->GetCurrentValue(),
826              DownloadShelf::SMALL);
827        }
828      }
829    }
830
831    // Draw the icon image.
832    int icon_x, icon_y;
833
834    if (IsShowingWarningDialog()) {
835      icon_x = kLeftPadding + body_image_set->top_left->width();
836      icon_y = (height() - icon->height()) / 2;
837    } else {
838      icon_x = DownloadShelf::kSmallProgressIconOffset;
839      icon_y = DownloadShelf::kSmallProgressIconOffset;
840    }
841    icon_x = GetMirroredXWithWidthInView(icon_x, icon->width());
842    if (enabled()) {
843      canvas->DrawImageInt(*icon, icon_x, icon_y);
844    } else {
845      // Use an alpha to make the image look disabled.
846      SkPaint paint;
847      paint.setAlpha(120);
848      canvas->DrawImageInt(*icon, icon_x, icon_y, paint);
849    }
850  }
851}
852
853void DownloadItemView::OpenDownload() {
854  DCHECK(!IsShowingWarningDialog());
855  // We're interested in how long it takes users to open downloads.  If they
856  // open downloads super quickly, we should be concerned about clickjacking.
857  UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
858                           base::Time::Now() - creation_time_);
859  download()->OpenDownload();
860  UpdateAccessibleName();
861}
862
863bool DownloadItemView::BeginDownloadFeedback() {
864  SafeBrowsingService* sb_service = g_browser_process->safe_browsing_service();
865  if (!sb_service)
866    return false;
867  safe_browsing::DownloadProtectionService* download_protection_service =
868      sb_service->download_protection_service();
869  if (!download_protection_service)
870    return false;
871  UMA_HISTOGRAM_LONG_TIMES("clickjacking.report_and_discard_download",
872                           base::Time::Now() - creation_time_);
873  download_protection_service->feedback_service()->BeginFeedbackForDownload(
874      download());
875  // WARNING: we are deleted at this point.  Don't access 'this'.
876  return true;
877}
878
879void DownloadItemView::LoadIcon() {
880  IconManager* im = g_browser_process->icon_manager();
881  last_download_item_path_ = download()->GetTargetFilePath();
882  im->LoadIcon(last_download_item_path_,
883               IconLoader::SMALL,
884               base::Bind(&DownloadItemView::OnExtractIconComplete,
885                          base::Unretained(this)),
886               &cancelable_task_tracker_);
887}
888
889void DownloadItemView::LoadIconIfItemPathChanged() {
890  base::FilePath current_download_path = download()->GetTargetFilePath();
891  if (last_download_item_path_ == current_download_path)
892    return;
893
894  LoadIcon();
895}
896
897void DownloadItemView::UpdateColorsFromTheme() {
898  if (dangerous_download_label_ && GetThemeProvider()) {
899    dangerous_download_label_->SetEnabledColor(
900        GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT));
901  }
902}
903
904void DownloadItemView::ShowContextMenuImpl(const gfx::Point& p,
905                                           ui::MenuSourceType source_type) {
906  gfx::Point point = p;
907  gfx::Size size;
908
909  // Similar hack as in MenuButton.
910  // We're about to show the menu from a mouse press. By showing from the
911  // mouse press event we block RootView in mouse dispatching. This also
912  // appears to cause RootView to get a mouse pressed BEFORE the mouse
913  // release is seen, which means RootView sends us another mouse press no
914  // matter where the user pressed. To force RootView to recalculate the
915  // mouse target during the mouse press we explicitly set the mouse handler
916  // to NULL.
917  static_cast<views::internal::RootView*>(GetWidget()->GetRootView())->
918      SetMouseHandler(NULL);
919
920  // If |is_mouse_gesture| is false, |p| is ignored. The menu is shown aligned
921  // to drop down arrow button.
922  if (source_type != ui::MENU_SOURCE_MOUSE &&
923      source_type != ui::MENU_SOURCE_TOUCH) {
924    drop_down_pressed_ = true;
925    SetState(NORMAL, PUSHED);
926    point.SetPoint(drop_down_x_left_, box_y_);
927    size.SetSize(drop_down_x_right_ - drop_down_x_left_, box_height_);
928  }
929  // Post a task to release the button.  When we call the Run method on the menu
930  // below, it runs an inner message loop that might cause us to be deleted.
931  // Posting a task with a WeakPtr lets us safely handle the button release.
932  base::MessageLoop::current()->PostNonNestableTask(
933      FROM_HERE,
934      base::Bind(&DownloadItemView::ReleaseDropDown,
935                 weak_ptr_factory_.GetWeakPtr()));
936  views::View::ConvertPointToScreen(this, &point);
937
938  if (!context_menu_.get()) {
939    context_menu_.reset(
940        new DownloadShelfContextMenuView(download(), shelf_->GetNavigator()));
941  }
942  context_menu_->Run(GetWidget()->GetTopLevelWidget(),
943                     gfx::Rect(point, size), source_type);
944  // We could be deleted now.
945}
946
947void DownloadItemView::HandlePressEvent(const ui::LocatedEvent& event,
948                                        bool active_event) {
949  // The event should not activate us in dangerous mode.
950  if (mode_ == DANGEROUS_MODE)
951    return;
952
953  // Stop any completion animation.
954  if (complete_animation_.get() && complete_animation_->is_animating())
955    complete_animation_->End();
956
957  if (active_event) {
958    if (InDropDownButtonXCoordinateRange(event.x())) {
959      drop_down_pressed_ = true;
960      SetState(NORMAL, PUSHED);
961      // We are setting is_mouse_gesture to false when calling ShowContextMenu
962      // so that the positioning of the context menu will be similar to a
963      // keyboard invocation.  I.e. we want the menu to always be positioned
964      // next to the drop down button instead of the next to the pointer.
965      ShowContextMenuImpl(event.location(), ui::MENU_SOURCE_KEYBOARD);
966      // Once called, it is possible that *this was deleted (e.g.: due to
967      // invoking the 'Discard' action.)
968    } else if (!IsShowingWarningDialog()) {
969      SetState(PUSHED, NORMAL);
970    }
971  }
972}
973
974void DownloadItemView::HandleClickEvent(const ui::LocatedEvent& event,
975                                        bool active_event) {
976  // Mouse should not activate us in dangerous mode.
977  if (mode_ == DANGEROUS_MODE)
978    return;
979
980  if (active_event &&
981      !InDropDownButtonXCoordinateRange(event.x()) &&
982      !IsShowingWarningDialog()) {
983    OpenDownload();
984  }
985
986  SetState(NORMAL, NORMAL);
987}
988
989// Load an icon for the file type we're downloading, and animate any in progress
990// download state.
991void DownloadItemView::PaintImages(gfx::Canvas* canvas,
992                                   const gfx::ImageSkia* top_image,
993                                   const gfx::ImageSkia* center_image,
994                                   const gfx::ImageSkia* bottom_image,
995                                   int x, int y, int height, int width) {
996  int middle_height = height - top_image->height() - bottom_image->height();
997  // Draw the top.
998  canvas->DrawImageInt(*top_image,
999                       0, 0, top_image->width(), top_image->height(),
1000                       x, y, width, top_image->height(), false);
1001  y += top_image->height();
1002  // Draw the center.
1003  canvas->DrawImageInt(*center_image,
1004                       0, 0, center_image->width(), center_image->height(),
1005                       x, y, width, middle_height, false);
1006  y += middle_height;
1007  // Draw the bottom.
1008  canvas->DrawImageInt(*bottom_image,
1009                       0, 0, bottom_image->width(), bottom_image->height(),
1010                       x, y, width, bottom_image->height(), false);
1011}
1012
1013void DownloadItemView::SetState(State new_body_state, State new_drop_state) {
1014  // If we are showing a warning dialog, we don't change body state.
1015  if (IsShowingWarningDialog()) {
1016    new_body_state = NORMAL;
1017
1018    // Current body_state_ should always be NORMAL for warning dialogs.
1019    DCHECK_EQ(NORMAL, body_state_);
1020    // We shouldn't be calling SetState if we are in DANGEROUS_MODE.
1021    DCHECK_NE(DANGEROUS_MODE, mode_);
1022  }
1023  // Avoid extra SchedulePaint()s if the state is going to be the same.
1024  if (body_state_ == new_body_state && drop_down_state_ == new_drop_state)
1025    return;
1026
1027  AnimateStateTransition(body_state_, new_body_state,
1028                         body_hover_animation_.get());
1029  AnimateStateTransition(drop_down_state_, new_drop_state,
1030                         drop_hover_animation_.get());
1031  body_state_ = new_body_state;
1032  drop_down_state_ = new_drop_state;
1033  SchedulePaint();
1034}
1035
1036void DownloadItemView::ClearWarningDialog() {
1037  DCHECK(download()->GetDangerType() ==
1038         content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED);
1039  DCHECK(mode_ == DANGEROUS_MODE || mode_ == MALICIOUS_MODE);
1040
1041  mode_ = NORMAL_MODE;
1042  body_state_ = NORMAL;
1043  drop_down_state_ = NORMAL;
1044
1045  // Remove the views used by the warning dialog.
1046  if (save_button_) {
1047    RemoveChildView(save_button_);
1048    delete save_button_;
1049    save_button_ = NULL;
1050  }
1051  RemoveChildView(discard_button_);
1052  delete discard_button_;
1053  discard_button_ = NULL;
1054  RemoveChildView(dangerous_download_label_);
1055  delete dangerous_download_label_;
1056  dangerous_download_label_ = NULL;
1057  dangerous_download_label_sized_ = false;
1058  cached_button_size_.SetSize(0,0);
1059
1060  // Set the accessible name back to the status and filename instead of the
1061  // download warning.
1062  UpdateAccessibleName();
1063  UpdateDropDownButtonPosition();
1064
1065  // We need to load the icon now that the download has the real path.
1066  LoadIcon();
1067
1068  // Force the shelf to layout again as our size has changed.
1069  shelf_->Layout();
1070  shelf_->SchedulePaint();
1071
1072  TooltipTextChanged();
1073}
1074
1075void DownloadItemView::ShowWarningDialog() {
1076  DCHECK(mode_ != DANGEROUS_MODE && mode_ != MALICIOUS_MODE);
1077  mode_ = ((model_.IsMalicious()) ? MALICIOUS_MODE : DANGEROUS_MODE);
1078
1079  body_state_ = NORMAL;
1080  drop_down_state_ = NORMAL;
1081  if (mode_ == DANGEROUS_MODE) {
1082    save_button_ = new views::LabelButton(
1083        this, model_.GetWarningConfirmButtonText());
1084    save_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
1085    AddChildView(save_button_);
1086  }
1087  if (model_.ShouldAllowDownloadFeedback()) {
1088    safe_browsing::DownloadFeedbackService::RecordFeedbackButtonShown(
1089        download()->GetDangerType());
1090    discard_button_ = new views::LabelButton(
1091        this, l10n_util::GetStringUTF16(IDS_REPORT_AND_DISCARD_DOWNLOAD));
1092  } else {
1093    discard_button_ = new views::LabelButton(
1094        this, l10n_util::GetStringUTF16(IDS_DISCARD_DOWNLOAD));
1095  }
1096  discard_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
1097  AddChildView(discard_button_);
1098
1099  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
1100  switch (download()->GetDangerType()) {
1101    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
1102    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
1103    case content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
1104    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
1105      warning_icon_ = rb.GetImageSkiaNamed(IDR_SAFEBROWSING_WARNING);
1106      break;
1107
1108    case content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS:
1109    case content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
1110    case content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED:
1111    case content::DOWNLOAD_DANGER_TYPE_MAX:
1112      NOTREACHED();
1113      // fallthrough
1114
1115    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
1116    case content::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED:
1117      warning_icon_ = rb.GetImageSkiaNamed(IDR_WARNING);
1118  }
1119  string16 dangerous_label = model_.GetWarningText(font_list_, kTextWidth);
1120  dangerous_download_label_ = new views::Label(dangerous_label);
1121  dangerous_download_label_->SetMultiLine(true);
1122  dangerous_download_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
1123  dangerous_download_label_->SetAutoColorReadabilityEnabled(false);
1124  AddChildView(dangerous_download_label_);
1125  SizeLabelToMinWidth();
1126  UpdateDropDownButtonPosition();
1127  TooltipTextChanged();
1128}
1129
1130gfx::Size DownloadItemView::GetButtonSize() {
1131  DCHECK(discard_button_ && (mode_ == MALICIOUS_MODE || save_button_));
1132  gfx::Size size;
1133
1134  // We cache the size when successfully retrieved, not for performance reasons
1135  // but because if this DownloadItemView is being animated while the tab is
1136  // not showing, the native buttons are not parented and their preferred size
1137  // is 0, messing-up the layout.
1138  if (cached_button_size_.width() != 0)
1139    return cached_button_size_;
1140
1141  if (save_button_)
1142    size = save_button_->GetMinimumSize();
1143  gfx::Size discard_size = discard_button_->GetMinimumSize();
1144
1145  size.SetSize(std::max(size.width(), discard_size.width()),
1146               std::max(size.height(), discard_size.height()));
1147
1148  if (size.width() != 0)
1149    cached_button_size_ = size;
1150
1151  return size;
1152}
1153
1154// This method computes the minimum width of the label for displaying its text
1155// on 2 lines.  It just breaks the string in 2 lines on the spaces and keeps the
1156// configuration with minimum width.
1157void DownloadItemView::SizeLabelToMinWidth() {
1158  if (dangerous_download_label_sized_)
1159    return;
1160
1161  string16 label_text = dangerous_download_label_->text();
1162  TrimWhitespace(label_text, TRIM_ALL, &label_text);
1163  DCHECK_EQ(string16::npos, label_text.find('\n'));
1164
1165  // Make the label big so that GetPreferredSize() is not constrained by the
1166  // current width.
1167  dangerous_download_label_->SetBounds(0, 0, 1000, 1000);
1168
1169  // Use a const string from here. BreakIterator requies that text.data() not
1170  // change during its lifetime.
1171  const string16 original_text(label_text);
1172  // Using BREAK_WORD can work in most cases, but it can also break
1173  // lines where it should not. Using BREAK_LINE is safer although
1174  // slower for Chinese/Japanese. This is not perf-critical at all, though.
1175  base::i18n::BreakIterator iter(original_text,
1176                                 base::i18n::BreakIterator::BREAK_LINE);
1177  bool status = iter.Init();
1178  DCHECK(status);
1179
1180  string16 prev_text = original_text;
1181  gfx::Size size = dangerous_download_label_->GetPreferredSize();
1182  int min_width = size.width();
1183
1184  // Go through the string and try each line break (starting with no line break)
1185  // searching for the optimal line break position.  Stop if we find one that
1186  // yields one that is less than kDangerousTextWidth wide.  This is to prevent
1187  // a short string (e.g.: "This file is malicious") from being broken up
1188  // unnecessarily.
1189  while (iter.Advance() && min_width > kDangerousTextWidth) {
1190    size_t pos = iter.pos();
1191    if (pos >= original_text.length())
1192      break;
1193    string16 current_text = original_text;
1194    // This can be a low surrogate codepoint, but u_isUWhiteSpace will
1195    // return false and inserting a new line after a surrogate pair
1196    // is perfectly ok.
1197    char16 line_end_char = current_text[pos - 1];
1198    if (u_isUWhiteSpace(line_end_char))
1199      current_text.replace(pos - 1, 1, 1, char16('\n'));
1200    else
1201      current_text.insert(pos, 1, char16('\n'));
1202    dangerous_download_label_->SetText(current_text);
1203    size = dangerous_download_label_->GetPreferredSize();
1204
1205    // If the width is growing again, it means we passed the optimal width spot.
1206    if (size.width() > min_width) {
1207      dangerous_download_label_->SetText(prev_text);
1208      break;
1209    } else {
1210      min_width = size.width();
1211    }
1212    prev_text = current_text;
1213  }
1214
1215  dangerous_download_label_->SetBounds(0, 0, size.width(), size.height());
1216  dangerous_download_label_sized_ = true;
1217}
1218
1219void DownloadItemView::Reenable() {
1220  disabled_while_opening_ = false;
1221  SetEnabled(true);  // Triggers a repaint.
1222}
1223
1224void DownloadItemView::ReleaseDropDown() {
1225  drop_down_pressed_ = false;
1226  SetState(NORMAL, NORMAL);
1227}
1228
1229bool DownloadItemView::InDropDownButtonXCoordinateRange(int x) {
1230  if (x > drop_down_x_left_ && x < drop_down_x_right_)
1231    return true;
1232  return false;
1233}
1234
1235void DownloadItemView::UpdateAccessibleName() {
1236  string16 new_name;
1237  if (IsShowingWarningDialog()) {
1238    new_name = dangerous_download_label_->text();
1239  } else {
1240    new_name = status_text_ + char16(' ') +
1241        download()->GetFileNameToReportUser().LossyDisplayName();
1242  }
1243
1244  // If the name has changed, notify assistive technology that the name
1245  // has changed so they can announce it immediately.
1246  if (new_name != accessible_name_) {
1247    accessible_name_ = new_name;
1248    NotifyAccessibilityEvent(ui::AccessibilityTypes::EVENT_NAME_CHANGED, true);
1249  }
1250}
1251
1252void DownloadItemView::UpdateDropDownButtonPosition() {
1253  gfx::Size size = GetPreferredSize();
1254  if (base::i18n::IsRTL()) {
1255    // Drop down button is glued to the left of the download shelf.
1256    drop_down_x_left_ = 0;
1257    drop_down_x_right_ = normal_drop_down_image_set_.top->width();
1258  } else {
1259    // Drop down button is glued to the right of the download shelf.
1260    drop_down_x_left_ =
1261      size.width() - normal_drop_down_image_set_.top->width();
1262    drop_down_x_right_ = size.width();
1263  }
1264}
1265
1266void DownloadItemView::AnimateStateTransition(State from, State to,
1267                                              gfx::SlideAnimation* animation) {
1268  if (from == NORMAL && to == HOT) {
1269    animation->Show();
1270  } else if (from == HOT && to == NORMAL) {
1271    animation->Hide();
1272  } else if (from != to) {
1273    animation->Reset((to == HOT) ? 1.0 : 0.0);
1274  }
1275}
1276