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