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