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