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. 52 static const int kTextWidth = 140; // Pixels 53 static const int kDangerousTextWidth = 200; // Pixels 54 static const int kHorizontalTextPadding = 2; // Pixels 55 static const int kVerticalPadding = 3; // Pixels 56 static const int kVerticalTextSpacer = 2; // Pixels 57 static const int kVerticalTextPadding = 2; // Pixels 58 static 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. 62 static const int kLeftPadding = 0; // Pixels. 63 64 // The space between the Save and Discard buttons when prompting for a dangerous 65 // download. 66 static const int kButtonPadding = 5; // Pixels. 67 68 // The space on the left and right side of the dangerous download label. 69 static const int kLabelPadding = 4; // Pixels. 70 71 static const SkColor kFileNameDisabledColor = SkColorSetRGB(171, 192, 212); 72 73 // How long the 'download complete' animation should last for. 74 static const int kCompleteAnimationDurationMs = 2500; 75 76 // How long the 'download interrupted' animation should last for. 77 static const int kInterruptedAnimationDurationMs = 2500; 78 79 // How long we keep the item disabled after the user clicked it to open the 80 // downloaded item. 81 static 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. 86 static const double kDownloadItemLuminanceMod = 0.8; 87 88 using content::DownloadItem; 89 90 DownloadItemView::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_ = 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 (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 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 214 DownloadItemView::~DownloadItemView() { 215 StopDownloadProgress(); 216 download()->RemoveObserver(this); 217 } 218 219 // Progress animation handlers. 220 221 void DownloadItemView::UpdateDownloadProgress() { 222 progress_angle_ = 223 (progress_angle_ + DownloadShelf::kUnknownIncrementDegrees) % 224 DownloadShelf::kMaxDegrees; 225 SchedulePaint(); 226 } 227 228 void 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 236 void DownloadItemView::StopDownloadProgress() { 237 progress_timer_.Stop(); 238 } 239 240 void 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. 249 void 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 316 void DownloadItemView::OnDownloadDestroyed(DownloadItem* download) { 317 shelf_->RemoveDownloadView(this); // This will delete us! 318 } 319 320 void 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. 335 void 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 358 gfx::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, DownloadShelf::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 += DownloadShelf::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. 397 bool DownloadItemView::OnMousePressed(const ui::MouseEvent& event) { 398 HandlePressEvent(event, event.IsOnlyLeftMouseButton()); 399 return true; 400 } 401 402 // Handle drag (file copy) operations. 403 bool 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 429 void DownloadItemView::OnMouseReleased(const ui::MouseEvent& event) { 430 HandleClickEvent(event, event.IsOnlyLeftMouseButton()); 431 } 432 433 void 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 446 void 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 455 void 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 463 bool 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 476 bool 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 488 void 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 498 void DownloadItemView::OnThemeChanged() { 499 UpdateColorsFromTheme(); 500 } 501 502 void 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 519 void 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 528 void 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 547 void 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 // 590 void 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 DownloadShelf::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 DownloadShelf::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 DownloadShelf::PaintDownloadProgress(canvas, 801 this, 802 0, 803 0, 804 progress_angle_, 805 model_.PercentComplete(), 806 DownloadShelf::SMALL); 807 } else if (complete_animation_.get() && 808 complete_animation_->is_animating()) { 809 if (state == DownloadItem::INTERRUPTED) { 810 DownloadShelf::PaintDownloadInterrupted( 811 canvas, 812 this, 813 0, 814 0, 815 complete_animation_->GetCurrentValue(), 816 DownloadShelf::SMALL); 817 } else { 818 DCHECK_EQ(DownloadItem::COMPLETE, state); 819 DownloadShelf::PaintDownloadComplete( 820 canvas, 821 this, 822 0, 823 0, 824 complete_animation_->GetCurrentValue(), 825 DownloadShelf::SMALL); 826 } 827 } 828 } 829 830 // Draw the icon image. 831 int icon_x, icon_y; 832 833 if (IsShowingWarningDialog()) { 834 icon_x = kLeftPadding + body_image_set->top_left->width(); 835 icon_y = (height() - icon->height()) / 2; 836 } else { 837 icon_x = DownloadShelf::kSmallProgressIconOffset; 838 icon_y = DownloadShelf::kSmallProgressIconOffset; 839 } 840 icon_x = GetMirroredXWithWidthInView(icon_x, icon->width()); 841 if (enabled()) { 842 canvas->DrawImageInt(*icon, icon_x, icon_y); 843 } else { 844 // Use an alpha to make the image look disabled. 845 SkPaint paint; 846 paint.setAlpha(120); 847 canvas->DrawImageInt(*icon, icon_x, icon_y, paint); 848 } 849 } 850 } 851 852 void DownloadItemView::OpenDownload() { 853 DCHECK(!IsShowingWarningDialog()); 854 // We're interested in how long it takes users to open downloads. If they 855 // open downloads super quickly, we should be concerned about clickjacking. 856 UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download", 857 base::Time::Now() - creation_time_); 858 download()->OpenDownload(); 859 UpdateAccessibleName(); 860 } 861 862 bool DownloadItemView::BeginDownloadFeedback() { 863 SafeBrowsingService* sb_service = g_browser_process->safe_browsing_service(); 864 if (!sb_service) 865 return false; 866 safe_browsing::DownloadProtectionService* download_protection_service = 867 sb_service->download_protection_service(); 868 if (!download_protection_service) 869 return false; 870 UMA_HISTOGRAM_LONG_TIMES("clickjacking.report_and_discard_download", 871 base::Time::Now() - creation_time_); 872 download_protection_service->feedback_service()->BeginFeedbackForDownload( 873 download()); 874 // WARNING: we are deleted at this point. Don't access 'this'. 875 return true; 876 } 877 878 void DownloadItemView::LoadIcon() { 879 IconManager* im = g_browser_process->icon_manager(); 880 last_download_item_path_ = download()->GetTargetFilePath(); 881 im->LoadIcon(last_download_item_path_, 882 IconLoader::SMALL, 883 base::Bind(&DownloadItemView::OnExtractIconComplete, 884 base::Unretained(this)), 885 &cancelable_task_tracker_); 886 } 887 888 void DownloadItemView::LoadIconIfItemPathChanged() { 889 base::FilePath current_download_path = download()->GetTargetFilePath(); 890 if (last_download_item_path_ == current_download_path) 891 return; 892 893 LoadIcon(); 894 } 895 896 void DownloadItemView::UpdateColorsFromTheme() { 897 if (dangerous_download_label_ && GetThemeProvider()) { 898 dangerous_download_label_->SetEnabledColor( 899 GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT)); 900 } 901 } 902 903 void DownloadItemView::ShowContextMenuImpl(const gfx::Point& p, 904 ui::MenuSourceType source_type) { 905 gfx::Point point = p; 906 gfx::Size size; 907 908 // Similar hack as in MenuButton. 909 // We're about to show the menu from a mouse press. By showing from the 910 // mouse press event we block RootView in mouse dispatching. This also 911 // appears to cause RootView to get a mouse pressed BEFORE the mouse 912 // release is seen, which means RootView sends us another mouse press no 913 // matter where the user pressed. To force RootView to recalculate the 914 // mouse target during the mouse press we explicitly set the mouse handler 915 // to NULL. 916 static_cast<views::internal::RootView*>(GetWidget()->GetRootView())-> 917 SetMouseHandler(NULL); 918 919 // If |is_mouse_gesture| is false, |p| is ignored. The menu is shown aligned 920 // to drop down arrow button. 921 if (source_type != ui::MENU_SOURCE_MOUSE && 922 source_type != ui::MENU_SOURCE_TOUCH) { 923 drop_down_pressed_ = true; 924 SetState(NORMAL, PUSHED); 925 point.SetPoint(drop_down_x_left_, box_y_); 926 size.SetSize(drop_down_x_right_ - drop_down_x_left_, box_height_); 927 } 928 // Post a task to release the button. When we call the Run method on the menu 929 // below, it runs an inner message loop that might cause us to be deleted. 930 // Posting a task with a WeakPtr lets us safely handle the button release. 931 base::MessageLoop::current()->PostNonNestableTask( 932 FROM_HERE, 933 base::Bind(&DownloadItemView::ReleaseDropDown, 934 weak_ptr_factory_.GetWeakPtr())); 935 views::View::ConvertPointToScreen(this, &point); 936 937 if (!context_menu_.get()) { 938 context_menu_.reset( 939 new DownloadShelfContextMenuView(download(), shelf_->GetNavigator())); 940 } 941 context_menu_->Run(GetWidget()->GetTopLevelWidget(), 942 gfx::Rect(point, size), source_type); 943 // We could be deleted now. 944 } 945 946 void DownloadItemView::HandlePressEvent(const ui::LocatedEvent& event, 947 bool active_event) { 948 // The event should not activate us in dangerous mode. 949 if (mode_ == DANGEROUS_MODE) 950 return; 951 952 // Stop any completion animation. 953 if (complete_animation_.get() && complete_animation_->is_animating()) 954 complete_animation_->End(); 955 956 if (active_event) { 957 if (InDropDownButtonXCoordinateRange(event.x())) { 958 drop_down_pressed_ = true; 959 SetState(NORMAL, PUSHED); 960 // We are setting is_mouse_gesture to false when calling ShowContextMenu 961 // so that the positioning of the context menu will be similar to a 962 // keyboard invocation. I.e. we want the menu to always be positioned 963 // next to the drop down button instead of the next to the pointer. 964 ShowContextMenuImpl(event.location(), ui::MENU_SOURCE_KEYBOARD); 965 // Once called, it is possible that *this was deleted (e.g.: due to 966 // invoking the 'Discard' action.) 967 } else if (!IsShowingWarningDialog()) { 968 SetState(PUSHED, NORMAL); 969 } 970 } 971 } 972 973 void DownloadItemView::HandleClickEvent(const ui::LocatedEvent& event, 974 bool active_event) { 975 // Mouse should not activate us in dangerous mode. 976 if (mode_ == DANGEROUS_MODE) 977 return; 978 979 if (active_event && 980 !InDropDownButtonXCoordinateRange(event.x()) && 981 !IsShowingWarningDialog()) { 982 OpenDownload(); 983 } 984 985 SetState(NORMAL, NORMAL); 986 } 987 988 // Load an icon for the file type we're downloading, and animate any in progress 989 // download state. 990 void DownloadItemView::PaintImages(gfx::Canvas* canvas, 991 const gfx::ImageSkia* top_image, 992 const gfx::ImageSkia* center_image, 993 const gfx::ImageSkia* bottom_image, 994 int x, int y, int height, int width) { 995 int middle_height = height - top_image->height() - bottom_image->height(); 996 // Draw the top. 997 canvas->DrawImageInt(*top_image, 998 0, 0, top_image->width(), top_image->height(), 999 x, y, width, top_image->height(), false); 1000 y += top_image->height(); 1001 // Draw the center. 1002 canvas->DrawImageInt(*center_image, 1003 0, 0, center_image->width(), center_image->height(), 1004 x, y, width, middle_height, false); 1005 y += middle_height; 1006 // Draw the bottom. 1007 canvas->DrawImageInt(*bottom_image, 1008 0, 0, bottom_image->width(), bottom_image->height(), 1009 x, y, width, bottom_image->height(), false); 1010 } 1011 1012 void DownloadItemView::SetState(State new_body_state, State new_drop_state) { 1013 // If we are showing a warning dialog, we don't change body state. 1014 if (IsShowingWarningDialog()) { 1015 new_body_state = NORMAL; 1016 1017 // Current body_state_ should always be NORMAL for warning dialogs. 1018 DCHECK_EQ(NORMAL, body_state_); 1019 // We shouldn't be calling SetState if we are in DANGEROUS_MODE. 1020 DCHECK_NE(DANGEROUS_MODE, mode_); 1021 } 1022 // Avoid extra SchedulePaint()s if the state is going to be the same. 1023 if (body_state_ == new_body_state && drop_down_state_ == new_drop_state) 1024 return; 1025 1026 AnimateStateTransition(body_state_, new_body_state, 1027 body_hover_animation_.get()); 1028 AnimateStateTransition(drop_down_state_, new_drop_state, 1029 drop_hover_animation_.get()); 1030 body_state_ = new_body_state; 1031 drop_down_state_ = new_drop_state; 1032 SchedulePaint(); 1033 } 1034 1035 void DownloadItemView::ClearWarningDialog() { 1036 DCHECK(download()->GetDangerType() == 1037 content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED); 1038 DCHECK(mode_ == DANGEROUS_MODE || mode_ == MALICIOUS_MODE); 1039 1040 mode_ = NORMAL_MODE; 1041 body_state_ = NORMAL; 1042 drop_down_state_ = NORMAL; 1043 1044 // Remove the views used by the warning dialog. 1045 if (save_button_) { 1046 RemoveChildView(save_button_); 1047 delete save_button_; 1048 save_button_ = NULL; 1049 } 1050 RemoveChildView(discard_button_); 1051 delete discard_button_; 1052 discard_button_ = NULL; 1053 RemoveChildView(dangerous_download_label_); 1054 delete dangerous_download_label_; 1055 dangerous_download_label_ = NULL; 1056 dangerous_download_label_sized_ = false; 1057 cached_button_size_.SetSize(0,0); 1058 1059 // Set the accessible name back to the status and filename instead of the 1060 // download warning. 1061 UpdateAccessibleName(); 1062 UpdateDropDownButtonPosition(); 1063 1064 // We need to load the icon now that the download has the real path. 1065 LoadIcon(); 1066 1067 // Force the shelf to layout again as our size has changed. 1068 shelf_->Layout(); 1069 shelf_->SchedulePaint(); 1070 1071 TooltipTextChanged(); 1072 } 1073 1074 void DownloadItemView::ShowWarningDialog() { 1075 DCHECK(mode_ != DANGEROUS_MODE && mode_ != MALICIOUS_MODE); 1076 mode_ = ((model_.IsMalicious()) ? MALICIOUS_MODE : DANGEROUS_MODE); 1077 1078 body_state_ = NORMAL; 1079 drop_down_state_ = NORMAL; 1080 if (mode_ == DANGEROUS_MODE) { 1081 save_button_ = new views::LabelButton( 1082 this, model_.GetWarningConfirmButtonText()); 1083 save_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON); 1084 AddChildView(save_button_); 1085 } 1086 if (model_.ShouldAllowDownloadFeedback()) { 1087 safe_browsing::DownloadFeedbackService::RecordFeedbackButtonShown( 1088 download()->GetDangerType()); 1089 discard_button_ = new views::LabelButton( 1090 this, l10n_util::GetStringUTF16(IDS_REPORT_AND_DISCARD_DOWNLOAD)); 1091 } else { 1092 discard_button_ = new views::LabelButton( 1093 this, l10n_util::GetStringUTF16(IDS_DISCARD_DOWNLOAD)); 1094 } 1095 discard_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON); 1096 AddChildView(discard_button_); 1097 1098 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 1099 switch (download()->GetDangerType()) { 1100 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL: 1101 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT: 1102 case content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT: 1103 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST: 1104 warning_icon_ = rb.GetImageSkiaNamed(IDR_SAFEBROWSING_WARNING); 1105 break; 1106 1107 case content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS: 1108 case content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT: 1109 case content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED: 1110 case content::DOWNLOAD_DANGER_TYPE_MAX: 1111 NOTREACHED(); 1112 // fallthrough 1113 1114 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE: 1115 case content::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED: 1116 warning_icon_ = rb.GetImageSkiaNamed(IDR_WARNING); 1117 } 1118 string16 dangerous_label = model_.GetWarningText(font_, kTextWidth); 1119 dangerous_download_label_ = new views::Label(dangerous_label); 1120 dangerous_download_label_->SetMultiLine(true); 1121 dangerous_download_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 1122 dangerous_download_label_->SetAutoColorReadabilityEnabled(false); 1123 AddChildView(dangerous_download_label_); 1124 SizeLabelToMinWidth(); 1125 UpdateDropDownButtonPosition(); 1126 TooltipTextChanged(); 1127 } 1128 1129 gfx::Size DownloadItemView::GetButtonSize() { 1130 DCHECK(discard_button_ && (mode_ == MALICIOUS_MODE || save_button_)); 1131 gfx::Size size; 1132 1133 // We cache the size when successfully retrieved, not for performance reasons 1134 // but because if this DownloadItemView is being animated while the tab is 1135 // not showing, the native buttons are not parented and their preferred size 1136 // is 0, messing-up the layout. 1137 if (cached_button_size_.width() != 0) 1138 return cached_button_size_; 1139 1140 if (save_button_) 1141 size = save_button_->GetMinimumSize(); 1142 gfx::Size discard_size = discard_button_->GetMinimumSize(); 1143 1144 size.SetSize(std::max(size.width(), discard_size.width()), 1145 std::max(size.height(), discard_size.height())); 1146 1147 if (size.width() != 0) 1148 cached_button_size_ = size; 1149 1150 return size; 1151 } 1152 1153 // This method computes the minimum width of the label for displaying its text 1154 // on 2 lines. It just breaks the string in 2 lines on the spaces and keeps the 1155 // configuration with minimum width. 1156 void DownloadItemView::SizeLabelToMinWidth() { 1157 if (dangerous_download_label_sized_) 1158 return; 1159 1160 string16 label_text = dangerous_download_label_->text(); 1161 TrimWhitespace(label_text, TRIM_ALL, &label_text); 1162 DCHECK_EQ(string16::npos, label_text.find('\n')); 1163 1164 // Make the label big so that GetPreferredSize() is not constrained by the 1165 // current width. 1166 dangerous_download_label_->SetBounds(0, 0, 1000, 1000); 1167 1168 // Use a const string from here. BreakIterator requies that text.data() not 1169 // change during its lifetime. 1170 const string16 original_text(label_text); 1171 // Using BREAK_WORD can work in most cases, but it can also break 1172 // lines where it should not. Using BREAK_LINE is safer although 1173 // slower for Chinese/Japanese. This is not perf-critical at all, though. 1174 base::i18n::BreakIterator iter(original_text, 1175 base::i18n::BreakIterator::BREAK_LINE); 1176 bool status = iter.Init(); 1177 DCHECK(status); 1178 1179 string16 prev_text = original_text; 1180 gfx::Size size = dangerous_download_label_->GetPreferredSize(); 1181 int min_width = size.width(); 1182 1183 // Go through the string and try each line break (starting with no line break) 1184 // searching for the optimal line break position. Stop if we find one that 1185 // yields one that is less than kDangerousTextWidth wide. This is to prevent 1186 // a short string (e.g.: "This file is malicious") from being broken up 1187 // unnecessarily. 1188 while (iter.Advance() && min_width > kDangerousTextWidth) { 1189 size_t pos = iter.pos(); 1190 if (pos >= original_text.length()) 1191 break; 1192 string16 current_text = original_text; 1193 // This can be a low surrogate codepoint, but u_isUWhiteSpace will 1194 // return false and inserting a new line after a surrogate pair 1195 // is perfectly ok. 1196 char16 line_end_char = current_text[pos - 1]; 1197 if (u_isUWhiteSpace(line_end_char)) 1198 current_text.replace(pos - 1, 1, 1, char16('\n')); 1199 else 1200 current_text.insert(pos, 1, char16('\n')); 1201 dangerous_download_label_->SetText(current_text); 1202 size = dangerous_download_label_->GetPreferredSize(); 1203 1204 // If the width is growing again, it means we passed the optimal width spot. 1205 if (size.width() > min_width) { 1206 dangerous_download_label_->SetText(prev_text); 1207 break; 1208 } else { 1209 min_width = size.width(); 1210 } 1211 prev_text = current_text; 1212 } 1213 1214 dangerous_download_label_->SetBounds(0, 0, size.width(), size.height()); 1215 dangerous_download_label_sized_ = true; 1216 } 1217 1218 void DownloadItemView::Reenable() { 1219 disabled_while_opening_ = false; 1220 SetEnabled(true); // Triggers a repaint. 1221 } 1222 1223 void DownloadItemView::ReleaseDropDown() { 1224 drop_down_pressed_ = false; 1225 SetState(NORMAL, NORMAL); 1226 } 1227 1228 bool DownloadItemView::InDropDownButtonXCoordinateRange(int x) { 1229 if (x > drop_down_x_left_ && x < drop_down_x_right_) 1230 return true; 1231 return false; 1232 } 1233 1234 void DownloadItemView::UpdateAccessibleName() { 1235 string16 new_name; 1236 if (IsShowingWarningDialog()) { 1237 new_name = dangerous_download_label_->text(); 1238 } else { 1239 new_name = status_text_ + char16(' ') + 1240 download()->GetFileNameToReportUser().LossyDisplayName(); 1241 } 1242 1243 // If the name has changed, notify assistive technology that the name 1244 // has changed so they can announce it immediately. 1245 if (new_name != accessible_name_) { 1246 accessible_name_ = new_name; 1247 NotifyAccessibilityEvent(ui::AccessibilityTypes::EVENT_NAME_CHANGED, true); 1248 } 1249 } 1250 1251 void DownloadItemView::UpdateDropDownButtonPosition() { 1252 gfx::Size size = GetPreferredSize(); 1253 if (base::i18n::IsRTL()) { 1254 // Drop down button is glued to the left of the download shelf. 1255 drop_down_x_left_ = 0; 1256 drop_down_x_right_ = normal_drop_down_image_set_.top->width(); 1257 } else { 1258 // Drop down button is glued to the right of the download shelf. 1259 drop_down_x_left_ = 1260 size.width() - normal_drop_down_image_set_.top->width(); 1261 drop_down_x_right_ = size.width(); 1262 } 1263 } 1264 1265 void DownloadItemView::AnimateStateTransition(State from, State to, 1266 ui::SlideAnimation* animation) { 1267 if (from == NORMAL && to == HOT) { 1268 animation->Show(); 1269 } else if (from == HOT && to == NORMAL) { 1270 animation->Hide(); 1271 } else if (from != to) { 1272 animation->Reset((to == HOT) ? 1.0 : 0.0); 1273 } 1274 } 1275