download_item_view.cc revision ca12bfac764ba476d6cd062bf1dde12cc64c3f40
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/download_util.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/animation/slide_animation.h"
36#include "ui/base/events/event.h"
37#include "ui/base/l10n/l10n_util.h"
38#include "ui/base/resource/resource_bundle.h"
39#include "ui/base/text/text_elider.h"
40#include "ui/base/theme_provider.h"
41#include "ui/gfx/canvas.h"
42#include "ui/gfx/color_utils.h"
43#include "ui/gfx/image/image.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_(download_util::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_ = rb.GetFont(ui::ResourceBundle::BaseFont);
194  box_height_ = std::max<int>(2 * kVerticalPadding + font_.GetHeight() +
195                                  kVerticalTextPadding + font_.GetHeight(),
196                              2 * kVerticalPadding +
197                                  normal_body_image_set_.top_left->height() +
198                                  normal_body_image_set_.bottom_left->height());
199
200  if (download_util::kSmallProgressIconSize > box_height_)
201    box_y_ = (download_util::kSmallProgressIconSize - box_height_) / 2;
202  else
203    box_y_ = 0;
204
205  body_hover_animation_.reset(new ui::SlideAnimation(this));
206  drop_hover_animation_.reset(new ui::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_ = (progress_angle_ +
223                     download_util::kUnknownIncrementDegrees) %
224                    download_util::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(download_util::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 ui::SlideAnimation(this));
271        complete_animation_->SetSlideDuration(kInterruptedAnimationDurationMs);
272        complete_animation_->SetTweenType(ui::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 ui::SlideAnimation(this));
284        complete_animation_->SetSlideDuration(kCompleteAnimationDurationMs);
285        complete_animation_->SetTweenType(ui::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_, 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_.GetHeight() + kVerticalTextPadding;
363  // Then we increase the size if the progress icon doesn't fit.
364  height = std::max<int>(height, download_util::kSmallProgressIconSize);
365
366  if (IsShowingWarningDialog()) {
367    BodyImageSet* body_image_set =
368        (mode_ == DANGEROUS_MODE) ? &dangerous_mode_body_image_set_ :
369            &malicious_mode_body_image_set_;
370    width = kLeftPadding + body_image_set->top_left->width();
371    width += warning_icon_->width() + kLabelPadding;
372    width += dangerous_download_label_->width() + kLabelPadding;
373    gfx::Size button_size = GetButtonSize();
374    // Make sure the button fits.
375    height = std::max<int>(height, 2 * kVerticalPadding + button_size.height());
376    // Then we make sure the warning icon fits.
377    height = std::max<int>(height, 2 * kVerticalPadding +
378                                   warning_icon_->height());
379    if (save_button_)
380      width += button_size.width() + kButtonPadding;
381    width += button_size.width();
382    width += body_image_set->top_right->width();
383    if (mode_ == MALICIOUS_MODE)
384      width += normal_drop_down_image_set_.top->width();
385  } else {
386    width = kLeftPadding + normal_body_image_set_.top_left->width();
387    width += download_util::kSmallProgressIconSize;
388    width += kTextWidth;
389    width += normal_body_image_set_.top_right->width();
390    width += normal_drop_down_image_set_.top->width();
391  }
392  return gfx::Size(width, height);
393}
394
395// Handle a mouse click and open the context menu if the mouse is
396// over the drop-down region.
397bool DownloadItemView::OnMousePressed(const ui::MouseEvent& event) {
398  HandlePressEvent(event, event.IsOnlyLeftMouseButton());
399  return true;
400}
401
402// Handle drag (file copy) operations.
403bool DownloadItemView::OnMouseDragged(const ui::MouseEvent& event) {
404  // Mouse should not activate us in dangerous mode.
405  if (IsShowingWarningDialog())
406    return true;
407
408  if (!starting_drag_) {
409    starting_drag_ = true;
410    drag_start_point_ = event.location();
411  }
412  if (dragging_) {
413    if (download()->GetState() == DownloadItem::COMPLETE) {
414      IconManager* im = g_browser_process->icon_manager();
415      gfx::Image* icon = im->LookupIconFromFilepath(
416          download()->GetTargetFilePath(), IconLoader::SMALL);
417      if (icon) {
418        views::Widget* widget = GetWidget();
419        download_util::DragDownload(download(), icon,
420                                    widget ? widget->GetNativeView() : NULL);
421      }
422    }
423  } else if (ExceededDragThreshold(event.location() - drag_start_point_)) {
424    dragging_ = true;
425  }
426  return true;
427}
428
429void DownloadItemView::OnMouseReleased(const ui::MouseEvent& event) {
430  HandleClickEvent(event, event.IsOnlyLeftMouseButton());
431}
432
433void DownloadItemView::OnMouseCaptureLost() {
434  // Mouse should not activate us in dangerous mode.
435  if (mode_ == DANGEROUS_MODE)
436    return;
437
438  if (dragging_) {
439    // Starting a drag results in a MouseCaptureLost.
440    dragging_ = false;
441    starting_drag_ = false;
442  }
443  SetState(NORMAL, NORMAL);
444}
445
446void DownloadItemView::OnMouseMoved(const ui::MouseEvent& event) {
447  // Mouse should not activate us in dangerous mode.
448  if (mode_ == DANGEROUS_MODE)
449    return;
450
451  bool on_body = !InDropDownButtonXCoordinateRange(event.x());
452  SetState(on_body ? HOT : NORMAL, on_body ? NORMAL : HOT);
453}
454
455void DownloadItemView::OnMouseExited(const ui::MouseEvent& event) {
456  // Mouse should not activate us in dangerous mode.
457  if (mode_ == DANGEROUS_MODE)
458    return;
459
460  SetState(NORMAL, drop_down_pressed_ ? PUSHED : NORMAL);
461}
462
463bool DownloadItemView::OnKeyPressed(const ui::KeyEvent& event) {
464  // Key press should not activate us in dangerous mode.
465  if (IsShowingWarningDialog())
466    return true;
467
468  if (event.key_code() == ui::VKEY_SPACE ||
469      event.key_code() == ui::VKEY_RETURN) {
470    OpenDownload();
471    return true;
472  }
473  return false;
474}
475
476bool DownloadItemView::GetTooltipText(const gfx::Point& p,
477                                      string16* tooltip) const {
478  if (IsShowingWarningDialog()) {
479    tooltip->clear();
480    return false;
481  }
482
483  tooltip->assign(tooltip_text_);
484
485  return true;
486}
487
488void DownloadItemView::GetAccessibleState(ui::AccessibleViewState* state) {
489  state->name = accessible_name_;
490  state->role = ui::AccessibilityTypes::ROLE_PUSHBUTTON;
491  if (model_.IsDangerous()) {
492    state->state = ui::AccessibilityTypes::STATE_UNAVAILABLE;
493  } else {
494    state->state = ui::AccessibilityTypes::STATE_HASPOPUP;
495  }
496}
497
498void DownloadItemView::OnThemeChanged() {
499  UpdateColorsFromTheme();
500}
501
502void DownloadItemView::OnGestureEvent(ui::GestureEvent* event) {
503  if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
504    HandlePressEvent(*event, true);
505    event->SetHandled();
506    return;
507  }
508
509  if (event->type() == ui::ET_GESTURE_TAP) {
510    HandleClickEvent(*event, true);
511    event->SetHandled();
512    return;
513  }
514
515  SetState(NORMAL, NORMAL);
516  views::View::OnGestureEvent(event);
517}
518
519void DownloadItemView::ShowContextMenuForView(View* source,
520                                              const gfx::Point& point,
521                                              ui::MenuSourceType source_type) {
522  // |point| is in screen coordinates. So convert it to local coordinates first.
523  gfx::Point local_point = point;
524  ConvertPointFromScreen(this, &local_point);
525  ShowContextMenuImpl(local_point, source_type);
526}
527
528void DownloadItemView::ButtonPressed(
529    views::Button* sender, const ui::Event& event) {
530  if (sender == discard_button_) {
531    if (model_.ShouldAllowDownloadFeedback() && BeginDownloadFeedback())
532      return;
533    UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
534                             base::Time::Now() - creation_time_);
535    download()->Remove();
536    // WARNING: we are deleted at this point.  Don't access 'this'.
537  } else if (save_button_ && sender == save_button_) {
538    // The user has confirmed a dangerous download.  We'd record how quickly the
539    // user did this to detect whether we're being clickjacked.
540    UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
541                             base::Time::Now() - creation_time_);
542    // This will change the state and notify us.
543    download()->ValidateDangerousDownload();
544  }
545}
546
547void DownloadItemView::AnimationProgressed(const ui::Animation* animation) {
548  // We don't care if what animation (body button/drop button/complete),
549  // is calling back, as they all have to go through the same paint call.
550  SchedulePaint();
551}
552
553// The DownloadItemView can be in three major modes (NORMAL_MODE, DANGEROUS_MODE
554// and MALICIOUS_MODE).
555//
556// NORMAL_MODE: We are displaying an in-progress or completed download.
557// .-------------------------------+-.
558// | [icon] Filename               |v|
559// | [    ] Status                 | |
560// `-------------------------------+-'
561//  |  |                            \_ Drop down button. Invokes menu. Responds
562//  |  |                               to mouse. (NORMAL, HOT or PUSHED).
563//  |   \_ Icon is overlaid on top of in-progress animation.
564//   \_ Both the body and the drop down button respond to mouse hover and can be
565//      pushed (NORMAL, HOT or PUSHED).
566//
567// DANGEROUS_MODE: The file could be potentially dangerous.
568// .-------------------------------------------------------.
569// | [ ! ] [This type of file can  ]  [ Keep ] [ Discard ] |
570// | [   ] [destroy your computer..]  [      ] [         ] |
571// `-------------------------------------------------------'
572//  |  |    |                          |                 \_ No drop down button.
573//  |  |    |                           \_ Buttons are views::LabelButtons.
574//  |  |     \_ Text is in a label (dangerous_download_label_)
575//  |   \_ Warning icon.  No progress animation.
576//   \_ Body is static.  Doesn't respond to mouse hover or press. (NORMAL only)
577//
578// MALICIOUS_MODE: The file is known malware.
579// .---------------------------------------------+-.
580// | [ - ] [This file is malicious.] [ Discard ] |v|
581// | [   ] [                       ] [         ] | |-.
582// `---------------------------------------------+-' |
583//  |  |    |                         |            Drop down button. Responds to
584//  |  |    |                         |            mouse.(NORMAL, HOT or PUSHED)
585//  |  |    |                          \_ Button is a views::LabelButton.
586//  |  |     \_ Text is in a label (dangerous_download_label_)
587//  |   \_ Warning icon.  No progress animation.
588//   \_ Body is static.  Doesn't respond to mouse hover or press. (NORMAL only)
589//
590void DownloadItemView::OnPaint(gfx::Canvas* canvas) {
591  BodyImageSet* body_image_set = NULL;
592  switch (mode_) {
593    case NORMAL_MODE:
594      if (body_state_ == PUSHED)
595        body_image_set = &pushed_body_image_set_;
596      else                      // NORMAL or HOT
597        body_image_set = &normal_body_image_set_;
598      break;
599    case DANGEROUS_MODE:
600      body_image_set = &dangerous_mode_body_image_set_;
601      break;
602    case MALICIOUS_MODE:
603      body_image_set = &malicious_mode_body_image_set_;
604      break;
605    default:
606      NOTREACHED();
607  }
608
609  DropDownImageSet* drop_down_image_set = NULL;
610  switch (mode_) {
611    case NORMAL_MODE:
612    case MALICIOUS_MODE:
613      if (drop_down_state_ == PUSHED)
614        drop_down_image_set = &pushed_drop_down_image_set_;
615      else                        // NORMAL or HOT
616        drop_down_image_set = &normal_drop_down_image_set_;
617      break;
618    case DANGEROUS_MODE:
619      // We don't use a drop down button for mode_ == DANGEROUS_MODE.  So we let
620      // drop_down_image_set == NULL.
621      break;
622    default:
623      NOTREACHED();
624  }
625
626  int center_width = width() - kLeftPadding -
627                     body_image_set->left->width() -
628                     body_image_set->right->width() -
629                     (drop_down_image_set ?
630                        normal_drop_down_image_set_.center->width() :
631                        0);
632
633  // May be caused by animation.
634  if (center_width <= 0)
635    return;
636
637  // Draw status before button image to effectively lighten text.  No status for
638  // warning dialogs.
639  if (!IsShowingWarningDialog()) {
640    if (!status_text_.empty()) {
641      int mirrored_x = GetMirroredXWithWidthInView(
642          download_util::kSmallProgressIconSize, kTextWidth);
643      // Add font_.height() to compensate for title, which is drawn later.
644      int y = box_y_ + kVerticalPadding + font_.GetHeight() +
645              kVerticalTextPadding;
646      SkColor file_name_color = GetThemeProvider()->GetColor(
647          ThemeProperties::COLOR_BOOKMARK_TEXT);
648      // If text is light-on-dark, lightening it alone will do nothing.
649      // Therefore we mute luminance a wee bit before drawing in this case.
650      if (color_utils::RelativeLuminance(file_name_color) > 0.5)
651          file_name_color = SkColorSetRGB(
652              static_cast<int>(kDownloadItemLuminanceMod *
653                               SkColorGetR(file_name_color)),
654              static_cast<int>(kDownloadItemLuminanceMod *
655                               SkColorGetG(file_name_color)),
656              static_cast<int>(kDownloadItemLuminanceMod *
657                               SkColorGetB(file_name_color)));
658      canvas->DrawStringInt(status_text_, font_,
659                            file_name_color, mirrored_x, y, kTextWidth,
660                            font_.GetHeight());
661    }
662  }
663
664  // Paint the background images.
665  int x = kLeftPadding;
666  canvas->Save();
667  if (base::i18n::IsRTL()) {
668    // Since we do not have the mirrored images for
669    // (hot_)body_image_set->top_left, (hot_)body_image_set->left,
670    // (hot_)body_image_set->bottom_left, and drop_down_image_set,
671    // for RTL UI, we flip the canvas to draw those images mirrored.
672    // Consequently, we do not need to mirror the x-axis of those images.
673    canvas->Translate(gfx::Vector2d(width(), 0));
674    canvas->Scale(-1, 1);
675  }
676  PaintImages(canvas,
677              body_image_set->top_left, body_image_set->left,
678              body_image_set->bottom_left,
679              x, box_y_, box_height_, body_image_set->top_left->width());
680  x += body_image_set->top_left->width();
681  PaintImages(canvas,
682              body_image_set->top, body_image_set->center,
683              body_image_set->bottom,
684              x, box_y_, box_height_, center_width);
685  x += center_width;
686  PaintImages(canvas,
687              body_image_set->top_right, body_image_set->right,
688              body_image_set->bottom_right,
689              x, box_y_, box_height_, body_image_set->top_right->width());
690
691  // Overlay our body hot state. Warning dialogs don't display body a hot state.
692  if (!IsShowingWarningDialog() &&
693      body_hover_animation_->GetCurrentValue() > 0) {
694    canvas->SaveLayerAlpha(
695        static_cast<int>(body_hover_animation_->GetCurrentValue() * 255));
696
697    int x = kLeftPadding;
698    PaintImages(canvas,
699                hot_body_image_set_.top_left, hot_body_image_set_.left,
700                hot_body_image_set_.bottom_left,
701                x, box_y_, box_height_, hot_body_image_set_.top_left->width());
702    x += body_image_set->top_left->width();
703    PaintImages(canvas,
704                hot_body_image_set_.top, hot_body_image_set_.center,
705                hot_body_image_set_.bottom,
706                x, box_y_, box_height_, center_width);
707    x += center_width;
708    PaintImages(canvas,
709                hot_body_image_set_.top_right, hot_body_image_set_.right,
710                hot_body_image_set_.bottom_right,
711                x, box_y_, box_height_,
712                hot_body_image_set_.top_right->width());
713    canvas->Restore();
714  }
715
716  x += body_image_set->top_right->width();
717
718  // Paint the drop-down.
719  if (drop_down_image_set) {
720    PaintImages(canvas,
721                drop_down_image_set->top, drop_down_image_set->center,
722                drop_down_image_set->bottom,
723                x, box_y_, box_height_, drop_down_image_set->top->width());
724
725    // Overlay our drop-down hot state.
726    if (drop_hover_animation_->GetCurrentValue() > 0) {
727      canvas->SaveLayerAlpha(
728          static_cast<int>(drop_hover_animation_->GetCurrentValue() * 255));
729
730      PaintImages(canvas,
731                  drop_down_image_set->top, drop_down_image_set->center,
732                  drop_down_image_set->bottom,
733                  x, box_y_, box_height_, drop_down_image_set->top->width());
734
735      canvas->Restore();
736    }
737  }
738
739  // Restore the canvas to avoid file name etc. text are drawn flipped.
740  // Consequently, the x-axis of following canvas->DrawXXX() method should be
741  // mirrored so the text and images are down in the right positions.
742  canvas->Restore();
743
744  // Print the text, left aligned and always print the file extension.
745  // Last value of x was the end of the right image, just before the button.
746  // Note that in dangerous mode we use a label (as the text is multi-line).
747  if (!IsShowingWarningDialog()) {
748    string16 filename;
749    if (!disabled_while_opening_) {
750      filename = ui::ElideFilename(download()->GetFileNameToReportUser(),
751                                   font_, kTextWidth);
752    } else {
753      // First, Calculate the download status opening string width.
754      string16 status_string =
755          l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING, string16());
756      int status_string_width = font_.GetStringWidth(status_string);
757      // Then, elide the file name.
758      string16 filename_string =
759          ui::ElideFilename(download()->GetFileNameToReportUser(), font_,
760                            kTextWidth - status_string_width);
761      // Last, concat the whole string.
762      filename = l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
763                                            filename_string);
764    }
765
766    int mirrored_x = GetMirroredXWithWidthInView(
767        download_util::kSmallProgressIconSize, kTextWidth);
768    SkColor file_name_color = GetThemeProvider()->GetColor(
769        ThemeProperties::COLOR_BOOKMARK_TEXT);
770    int y =
771        box_y_ + (status_text_.empty() ?
772                  ((box_height_ - font_.GetHeight()) / 2) : kVerticalPadding);
773
774    // Draw the file's name.
775    canvas->DrawStringInt(filename, font_,
776                          enabled() ? file_name_color
777                                    : kFileNameDisabledColor,
778                          mirrored_x, y, kTextWidth, font_.GetHeight());
779  }
780
781  // Load the icon.
782  IconManager* im = g_browser_process->icon_manager();
783  gfx::Image* image = im->LookupIconFromFilepath(
784      download()->GetTargetFilePath(), IconLoader::SMALL);
785  const gfx::ImageSkia* icon = NULL;
786  if (IsShowingWarningDialog())
787    icon = warning_icon_;
788  else if (image)
789    icon = image->ToImageSkia();
790
791  // We count on the fact that the icon manager will cache the icons and if one
792  // is available, it will be cached here. We *don't* want to request the icon
793  // to be loaded here, since this will also get called if the icon can't be
794  // loaded, in which case LookupIcon will always be NULL. The loading will be
795  // triggered only when we think the status might change.
796  if (icon) {
797    if (!IsShowingWarningDialog()) {
798      DownloadItem::DownloadState state = download()->GetState();
799      if (state == DownloadItem::IN_PROGRESS) {
800        download_util::PaintDownloadProgress(canvas, this, 0, 0,
801                                             progress_angle_,
802                                             model_.PercentComplete(),
803                                             download_util::SMALL);
804      } else if (complete_animation_.get() &&
805                 complete_animation_->is_animating()) {
806        if (state == DownloadItem::INTERRUPTED) {
807          download_util::PaintDownloadInterrupted(canvas, this, 0, 0,
808              complete_animation_->GetCurrentValue(),
809              download_util::SMALL);
810        } else {
811          DCHECK_EQ(DownloadItem::COMPLETE, state);
812          download_util::PaintDownloadComplete(canvas, this, 0, 0,
813              complete_animation_->GetCurrentValue(),
814              download_util::SMALL);
815        }
816      }
817    }
818
819    // Draw the icon image.
820    int icon_x, icon_y;
821
822    if (IsShowingWarningDialog()) {
823      icon_x = kLeftPadding + body_image_set->top_left->width();
824      icon_y = (height() - icon->height()) / 2;
825    } else {
826      icon_x = download_util::kSmallProgressIconOffset;
827      icon_y = download_util::kSmallProgressIconOffset;
828    }
829    icon_x = GetMirroredXWithWidthInView(icon_x, icon->width());
830    if (enabled()) {
831      canvas->DrawImageInt(*icon, icon_x, icon_y);
832    } else {
833      // Use an alpha to make the image look disabled.
834      SkPaint paint;
835      paint.setAlpha(120);
836      canvas->DrawImageInt(*icon, icon_x, icon_y, paint);
837    }
838  }
839}
840
841void DownloadItemView::OpenDownload() {
842  DCHECK(!IsShowingWarningDialog());
843  // We're interested in how long it takes users to open downloads.  If they
844  // open downloads super quickly, we should be concerned about clickjacking.
845  UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
846                           base::Time::Now() - creation_time_);
847  download()->OpenDownload();
848  UpdateAccessibleName();
849}
850
851bool DownloadItemView::BeginDownloadFeedback() {
852  SafeBrowsingService* sb_service = g_browser_process->safe_browsing_service();
853  if (!sb_service)
854    return false;
855  safe_browsing::DownloadProtectionService* download_protection_service =
856      sb_service->download_protection_service();
857  if (!download_protection_service)
858    return false;
859  UMA_HISTOGRAM_LONG_TIMES("clickjacking.report_and_discard_download",
860                           base::Time::Now() - creation_time_);
861  download_protection_service->feedback_service()->BeginFeedbackForDownload(
862      download());
863  // WARNING: we are deleted at this point.  Don't access 'this'.
864  return true;
865}
866
867void DownloadItemView::LoadIcon() {
868  IconManager* im = g_browser_process->icon_manager();
869  last_download_item_path_ = download()->GetTargetFilePath();
870  im->LoadIcon(last_download_item_path_,
871               IconLoader::SMALL,
872               base::Bind(&DownloadItemView::OnExtractIconComplete,
873                          base::Unretained(this)),
874               &cancelable_task_tracker_);
875}
876
877void DownloadItemView::LoadIconIfItemPathChanged() {
878  base::FilePath current_download_path = download()->GetTargetFilePath();
879  if (last_download_item_path_ == current_download_path)
880    return;
881
882  LoadIcon();
883}
884
885void DownloadItemView::UpdateColorsFromTheme() {
886  if (dangerous_download_label_ && GetThemeProvider()) {
887    dangerous_download_label_->SetEnabledColor(
888        GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT));
889  }
890}
891
892void DownloadItemView::ShowContextMenuImpl(const gfx::Point& p,
893                                           ui::MenuSourceType source_type) {
894  gfx::Point point = p;
895  gfx::Size size;
896
897  // Similar hack as in MenuButton.
898  // We're about to show the menu from a mouse press. By showing from the
899  // mouse press event we block RootView in mouse dispatching. This also
900  // appears to cause RootView to get a mouse pressed BEFORE the mouse
901  // release is seen, which means RootView sends us another mouse press no
902  // matter where the user pressed. To force RootView to recalculate the
903  // mouse target during the mouse press we explicitly set the mouse handler
904  // to NULL.
905  static_cast<views::internal::RootView*>(GetWidget()->GetRootView())->
906      SetMouseHandler(NULL);
907
908  // If |is_mouse_gesture| is false, |p| is ignored. The menu is shown aligned
909  // to drop down arrow button.
910  if (source_type != ui::MENU_SOURCE_MOUSE &&
911      source_type != ui::MENU_SOURCE_TOUCH) {
912    drop_down_pressed_ = true;
913    SetState(NORMAL, PUSHED);
914    point.SetPoint(drop_down_x_left_, box_y_);
915    size.SetSize(drop_down_x_right_ - drop_down_x_left_, box_height_);
916  }
917  // Post a task to release the button.  When we call the Run method on the menu
918  // below, it runs an inner message loop that might cause us to be deleted.
919  // Posting a task with a WeakPtr lets us safely handle the button release.
920  base::MessageLoop::current()->PostNonNestableTask(
921      FROM_HERE,
922      base::Bind(&DownloadItemView::ReleaseDropDown,
923                 weak_ptr_factory_.GetWeakPtr()));
924  views::View::ConvertPointToScreen(this, &point);
925
926  if (!context_menu_.get()) {
927    context_menu_.reset(
928        new DownloadShelfContextMenuView(download(), shelf_->GetNavigator()));
929  }
930  context_menu_->Run(GetWidget()->GetTopLevelWidget(),
931                     gfx::Rect(point, size), source_type);
932  // We could be deleted now.
933}
934
935void DownloadItemView::HandlePressEvent(const ui::LocatedEvent& event,
936                                        bool active_event) {
937  // The event should not activate us in dangerous mode.
938  if (mode_ == DANGEROUS_MODE)
939    return;
940
941  // Stop any completion animation.
942  if (complete_animation_.get() && complete_animation_->is_animating())
943    complete_animation_->End();
944
945  if (active_event) {
946    if (InDropDownButtonXCoordinateRange(event.x())) {
947      drop_down_pressed_ = true;
948      SetState(NORMAL, PUSHED);
949      // We are setting is_mouse_gesture to false when calling ShowContextMenu
950      // so that the positioning of the context menu will be similar to a
951      // keyboard invocation.  I.e. we want the menu to always be positioned
952      // next to the drop down button instead of the next to the pointer.
953      ShowContextMenuImpl(event.location(), ui::MENU_SOURCE_KEYBOARD);
954      // Once called, it is possible that *this was deleted (e.g.: due to
955      // invoking the 'Discard' action.)
956    } else if (!IsShowingWarningDialog()) {
957      SetState(PUSHED, NORMAL);
958    }
959  }
960}
961
962void DownloadItemView::HandleClickEvent(const ui::LocatedEvent& event,
963                                        bool active_event) {
964  // Mouse should not activate us in dangerous mode.
965  if (mode_ == DANGEROUS_MODE)
966    return;
967
968  if (active_event &&
969      !InDropDownButtonXCoordinateRange(event.x()) &&
970      !IsShowingWarningDialog()) {
971    OpenDownload();
972  }
973
974  SetState(NORMAL, NORMAL);
975}
976
977// Load an icon for the file type we're downloading, and animate any in progress
978// download state.
979void DownloadItemView::PaintImages(gfx::Canvas* canvas,
980                                   const gfx::ImageSkia* top_image,
981                                   const gfx::ImageSkia* center_image,
982                                   const gfx::ImageSkia* bottom_image,
983                                   int x, int y, int height, int width) {
984  int middle_height = height - top_image->height() - bottom_image->height();
985  // Draw the top.
986  canvas->DrawImageInt(*top_image,
987                       0, 0, top_image->width(), top_image->height(),
988                       x, y, width, top_image->height(), false);
989  y += top_image->height();
990  // Draw the center.
991  canvas->DrawImageInt(*center_image,
992                       0, 0, center_image->width(), center_image->height(),
993                       x, y, width, middle_height, false);
994  y += middle_height;
995  // Draw the bottom.
996  canvas->DrawImageInt(*bottom_image,
997                       0, 0, bottom_image->width(), bottom_image->height(),
998                       x, y, width, bottom_image->height(), false);
999}
1000
1001void DownloadItemView::SetState(State new_body_state, State new_drop_state) {
1002  // If we are showing a warning dialog, we don't change body state.
1003  if (IsShowingWarningDialog()) {
1004    new_body_state = NORMAL;
1005
1006    // Current body_state_ should always be NORMAL for warning dialogs.
1007    DCHECK_EQ(NORMAL, body_state_);
1008    // We shouldn't be calling SetState if we are in DANGEROUS_MODE.
1009    DCHECK_NE(DANGEROUS_MODE, mode_);
1010  }
1011  // Avoid extra SchedulePaint()s if the state is going to be the same.
1012  if (body_state_ == new_body_state && drop_down_state_ == new_drop_state)
1013    return;
1014
1015  AnimateStateTransition(body_state_, new_body_state,
1016                         body_hover_animation_.get());
1017  AnimateStateTransition(drop_down_state_, new_drop_state,
1018                         drop_hover_animation_.get());
1019  body_state_ = new_body_state;
1020  drop_down_state_ = new_drop_state;
1021  SchedulePaint();
1022}
1023
1024void DownloadItemView::ClearWarningDialog() {
1025  DCHECK(download()->GetDangerType() ==
1026         content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED);
1027  DCHECK(mode_ == DANGEROUS_MODE || mode_ == MALICIOUS_MODE);
1028
1029  mode_ = NORMAL_MODE;
1030  body_state_ = NORMAL;
1031  drop_down_state_ = NORMAL;
1032
1033  // Remove the views used by the warning dialog.
1034  if (save_button_) {
1035    RemoveChildView(save_button_);
1036    delete save_button_;
1037    save_button_ = NULL;
1038  }
1039  RemoveChildView(discard_button_);
1040  delete discard_button_;
1041  discard_button_ = NULL;
1042  RemoveChildView(dangerous_download_label_);
1043  delete dangerous_download_label_;
1044  dangerous_download_label_ = NULL;
1045  dangerous_download_label_sized_ = false;
1046  cached_button_size_.SetSize(0,0);
1047
1048  // Set the accessible name back to the status and filename instead of the
1049  // download warning.
1050  UpdateAccessibleName();
1051  UpdateDropDownButtonPosition();
1052
1053  // We need to load the icon now that the download has the real path.
1054  LoadIcon();
1055
1056  // Force the shelf to layout again as our size has changed.
1057  shelf_->Layout();
1058  shelf_->SchedulePaint();
1059
1060  TooltipTextChanged();
1061}
1062
1063void DownloadItemView::ShowWarningDialog() {
1064  DCHECK(mode_ != DANGEROUS_MODE && mode_ != MALICIOUS_MODE);
1065  mode_ = ((model_.IsMalicious()) ? MALICIOUS_MODE : DANGEROUS_MODE);
1066
1067  body_state_ = NORMAL;
1068  drop_down_state_ = NORMAL;
1069  if (mode_ == DANGEROUS_MODE) {
1070    save_button_ = new views::LabelButton(
1071        this, model_.GetWarningConfirmButtonText());
1072    save_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
1073    AddChildView(save_button_);
1074  }
1075  if (model_.ShouldAllowDownloadFeedback()) {
1076    safe_browsing::DownloadFeedbackService::RecordFeedbackButtonShown(
1077        download()->GetDangerType());
1078    discard_button_ = new views::LabelButton(
1079        this, l10n_util::GetStringUTF16(IDS_REPORT_AND_DISCARD_DOWNLOAD));
1080  } else {
1081    discard_button_ = new views::LabelButton(
1082        this, l10n_util::GetStringUTF16(IDS_DISCARD_DOWNLOAD));
1083  }
1084  discard_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
1085  AddChildView(discard_button_);
1086
1087  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
1088  // The dangerous download label text and icon are different under
1089  // different cases.
1090  if (mode_ == MALICIOUS_MODE) {
1091    warning_icon_ = rb.GetImageSkiaNamed(IDR_SAFEBROWSING_WARNING);
1092  } else {
1093    // The download file has dangerous file type (e.g.: an executable).
1094    warning_icon_ = rb.GetImageSkiaNamed(IDR_WARNING);
1095  }
1096  string16 dangerous_label = model_.GetWarningText(font_, kTextWidth);
1097  dangerous_download_label_ = new views::Label(dangerous_label);
1098  dangerous_download_label_->SetMultiLine(true);
1099  dangerous_download_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
1100  dangerous_download_label_->SetAutoColorReadabilityEnabled(false);
1101  AddChildView(dangerous_download_label_);
1102  SizeLabelToMinWidth();
1103  UpdateDropDownButtonPosition();
1104  TooltipTextChanged();
1105}
1106
1107gfx::Size DownloadItemView::GetButtonSize() {
1108  DCHECK(discard_button_ && (mode_ == MALICIOUS_MODE || save_button_));
1109  gfx::Size size;
1110
1111  // We cache the size when successfully retrieved, not for performance reasons
1112  // but because if this DownloadItemView is being animated while the tab is
1113  // not showing, the native buttons are not parented and their preferred size
1114  // is 0, messing-up the layout.
1115  if (cached_button_size_.width() != 0)
1116    return cached_button_size_;
1117
1118  if (save_button_)
1119    size = save_button_->GetMinimumSize();
1120  gfx::Size discard_size = discard_button_->GetMinimumSize();
1121
1122  size.SetSize(std::max(size.width(), discard_size.width()),
1123               std::max(size.height(), discard_size.height()));
1124
1125  if (size.width() != 0)
1126    cached_button_size_ = size;
1127
1128  return size;
1129}
1130
1131// This method computes the minimum width of the label for displaying its text
1132// on 2 lines.  It just breaks the string in 2 lines on the spaces and keeps the
1133// configuration with minimum width.
1134void DownloadItemView::SizeLabelToMinWidth() {
1135  if (dangerous_download_label_sized_)
1136    return;
1137
1138  string16 label_text = dangerous_download_label_->text();
1139  TrimWhitespace(label_text, TRIM_ALL, &label_text);
1140  DCHECK_EQ(string16::npos, label_text.find('\n'));
1141
1142  // Make the label big so that GetPreferredSize() is not constrained by the
1143  // current width.
1144  dangerous_download_label_->SetBounds(0, 0, 1000, 1000);
1145
1146  // Use a const string from here. BreakIterator requies that text.data() not
1147  // change during its lifetime.
1148  const string16 original_text(label_text);
1149  // Using BREAK_WORD can work in most cases, but it can also break
1150  // lines where it should not. Using BREAK_LINE is safer although
1151  // slower for Chinese/Japanese. This is not perf-critical at all, though.
1152  base::i18n::BreakIterator iter(original_text,
1153                                 base::i18n::BreakIterator::BREAK_LINE);
1154  bool status = iter.Init();
1155  DCHECK(status);
1156
1157  string16 prev_text = original_text;
1158  gfx::Size size = dangerous_download_label_->GetPreferredSize();
1159  int min_width = size.width();
1160
1161  // Go through the string and try each line break (starting with no line break)
1162  // searching for the optimal line break position.  Stop if we find one that
1163  // yields one that is less than kDangerousTextWidth wide.  This is to prevent
1164  // a short string (e.g.: "This file is malicious") from being broken up
1165  // unnecessarily.
1166  while (iter.Advance() && min_width > kDangerousTextWidth) {
1167    size_t pos = iter.pos();
1168    if (pos >= original_text.length())
1169      break;
1170    string16 current_text = original_text;
1171    // This can be a low surrogate codepoint, but u_isUWhiteSpace will
1172    // return false and inserting a new line after a surrogate pair
1173    // is perfectly ok.
1174    char16 line_end_char = current_text[pos - 1];
1175    if (u_isUWhiteSpace(line_end_char))
1176      current_text.replace(pos - 1, 1, 1, char16('\n'));
1177    else
1178      current_text.insert(pos, 1, char16('\n'));
1179    dangerous_download_label_->SetText(current_text);
1180    size = dangerous_download_label_->GetPreferredSize();
1181
1182    // If the width is growing again, it means we passed the optimal width spot.
1183    if (size.width() > min_width) {
1184      dangerous_download_label_->SetText(prev_text);
1185      break;
1186    } else {
1187      min_width = size.width();
1188    }
1189    prev_text = current_text;
1190  }
1191
1192  dangerous_download_label_->SetBounds(0, 0, size.width(), size.height());
1193  dangerous_download_label_sized_ = true;
1194}
1195
1196void DownloadItemView::Reenable() {
1197  disabled_while_opening_ = false;
1198  SetEnabled(true);  // Triggers a repaint.
1199}
1200
1201void DownloadItemView::ReleaseDropDown() {
1202  drop_down_pressed_ = false;
1203  SetState(NORMAL, NORMAL);
1204}
1205
1206bool DownloadItemView::InDropDownButtonXCoordinateRange(int x) {
1207  if (x > drop_down_x_left_ && x < drop_down_x_right_)
1208    return true;
1209  return false;
1210}
1211
1212void DownloadItemView::UpdateAccessibleName() {
1213  string16 new_name;
1214  if (IsShowingWarningDialog()) {
1215    new_name = dangerous_download_label_->text();
1216  } else {
1217    new_name = status_text_ + char16(' ') +
1218        download()->GetFileNameToReportUser().LossyDisplayName();
1219  }
1220
1221  // If the name has changed, notify assistive technology that the name
1222  // has changed so they can announce it immediately.
1223  if (new_name != accessible_name_) {
1224    accessible_name_ = new_name;
1225    NotifyAccessibilityEvent(ui::AccessibilityTypes::EVENT_NAME_CHANGED, true);
1226  }
1227}
1228
1229void DownloadItemView::UpdateDropDownButtonPosition() {
1230  gfx::Size size = GetPreferredSize();
1231  if (base::i18n::IsRTL()) {
1232    // Drop down button is glued to the left of the download shelf.
1233    drop_down_x_left_ = 0;
1234    drop_down_x_right_ = normal_drop_down_image_set_.top->width();
1235  } else {
1236    // Drop down button is glued to the right of the download shelf.
1237    drop_down_x_left_ =
1238      size.width() - normal_drop_down_image_set_.top->width();
1239    drop_down_x_right_ = size.width();
1240  }
1241}
1242
1243void DownloadItemView::AnimateStateTransition(State from, State to,
1244                                              ui::SlideAnimation* animation) {
1245  if (from == NORMAL && to == HOT) {
1246    animation->Show();
1247  } else if (from == HOT && to == NORMAL) {
1248    animation->Hide();
1249  } else if (from != to) {
1250    animation->Reset((to == HOT) ? 1.0 : 0.0);
1251  }
1252}
1253