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