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 "ui/app_list/views/app_list_item_view.h" 6 7 #include <algorithm> 8 9 #include "base/strings/utf_string_conversions.h" 10 #include "ui/accessibility/ax_view_state.h" 11 #include "ui/app_list/app_list_constants.h" 12 #include "ui/app_list/app_list_folder_item.h" 13 #include "ui/app_list/app_list_item.h" 14 #include "ui/app_list/views/apps_grid_view.h" 15 #include "ui/app_list/views/cached_label.h" 16 #include "ui/app_list/views/progress_bar_view.h" 17 #include "ui/base/dragdrop/drag_utils.h" 18 #include "ui/base/l10n/l10n_util.h" 19 #include "ui/base/resource/resource_bundle.h" 20 #include "ui/compositor/layer.h" 21 #include "ui/compositor/scoped_layer_animation_settings.h" 22 #include "ui/gfx/animation/throb_animation.h" 23 #include "ui/gfx/canvas.h" 24 #include "ui/gfx/font_list.h" 25 #include "ui/gfx/image/image_skia_operations.h" 26 #include "ui/gfx/point.h" 27 #include "ui/gfx/shadow_value.h" 28 #include "ui/gfx/transform_util.h" 29 #include "ui/strings/grit/ui_strings.h" 30 #include "ui/views/background.h" 31 #include "ui/views/controls/image_view.h" 32 #include "ui/views/controls/label.h" 33 #include "ui/views/controls/menu/menu_runner.h" 34 #include "ui/views/drag_controller.h" 35 36 namespace app_list { 37 38 namespace { 39 40 const int kTopPadding = 20; 41 const int kIconTitleSpacing = 7; 42 const int kProgressBarHorizontalPadding = 12; 43 44 // Radius of the folder dropping preview circle. 45 const int kFolderPreviewRadius = 40; 46 47 const int kLeftRightPaddingChars = 1; 48 49 // Scale to transform the icon when a drag starts. 50 const float kDraggingIconScale = 1.5f; 51 52 // Delay in milliseconds of when the dragging UI should be shown for mouse drag. 53 const int kMouseDragUIDelayInMs = 200; 54 55 const gfx::ShadowValues& GetIconShadows() { 56 CR_DEFINE_STATIC_LOCAL( 57 const gfx::ShadowValues, 58 icon_shadows, 59 (1, 60 gfx::ShadowValue(gfx::Point(0, 2), 2, SkColorSetARGB(0x24, 0, 0, 0)))); 61 return icon_shadows; 62 } 63 64 gfx::FontList GetFontList() { 65 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 66 const gfx::FontList& font_list = rb.GetFontList(kItemTextFontStyle); 67 // The font is different on each platform. The font size is adjusted on some 68 // platforms to keep a consistent look. 69 #if defined(OS_LINUX) && !defined(OS_CHROMEOS) 70 // Reducing the font size by 2 makes it the same as the Windows font size. 71 const int kFontSizeDelta = -2; 72 return font_list.DeriveWithSizeDelta(kFontSizeDelta); 73 #else 74 return font_list; 75 #endif 76 } 77 78 } // namespace 79 80 // static 81 const char AppListItemView::kViewClassName[] = "ui/app_list/AppListItemView"; 82 83 AppListItemView::AppListItemView(AppsGridView* apps_grid_view, 84 AppListItem* item) 85 : CustomButton(apps_grid_view), 86 is_folder_(item->GetItemType() == AppListFolderItem::kItemType), 87 is_in_folder_(item->IsInFolder()), 88 item_weak_(item), 89 apps_grid_view_(apps_grid_view), 90 icon_(new views::ImageView), 91 title_(new CachedLabel), 92 progress_bar_(new ProgressBarView), 93 ui_state_(UI_STATE_NORMAL), 94 touch_dragging_(false), 95 is_installing_(false), 96 is_highlighted_(false) { 97 icon_->set_interactive(false); 98 99 title_->SetBackgroundColor(0); 100 title_->SetAutoColorReadabilityEnabled(false); 101 title_->SetEnabledColor(kGridTitleColor); 102 103 static const gfx::FontList font_list = GetFontList(); 104 title_->SetFontList(font_list); 105 title_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 106 title_->Invalidate(); 107 SetTitleSubpixelAA(); 108 109 AddChildView(icon_); 110 AddChildView(title_); 111 AddChildView(progress_bar_); 112 113 SetIcon(item->icon(), item->has_shadow()); 114 SetItemName(base::UTF8ToUTF16(item->GetDisplayName()), 115 base::UTF8ToUTF16(item->name())); 116 SetItemIsInstalling(item->is_installing()); 117 SetItemIsHighlighted(item->highlighted()); 118 item->AddObserver(this); 119 120 set_context_menu_controller(this); 121 set_request_focus_on_press(false); 122 123 SetAnimationDuration(0); 124 } 125 126 AppListItemView::~AppListItemView() { 127 if (item_weak_) 128 item_weak_->RemoveObserver(this); 129 } 130 131 void AppListItemView::SetIcon(const gfx::ImageSkia& icon, bool has_shadow) { 132 // Clear icon and bail out if item icon is empty. 133 if (icon.isNull()) { 134 icon_->SetImage(NULL); 135 return; 136 } 137 138 gfx::ImageSkia resized(gfx::ImageSkiaOperations::CreateResizedImage( 139 icon, 140 skia::ImageOperations::RESIZE_BEST, 141 gfx::Size(kGridIconDimension, kGridIconDimension))); 142 if (has_shadow) { 143 gfx::ImageSkia shadow(gfx::ImageSkiaOperations::CreateImageWithDropShadow( 144 resized, GetIconShadows())); 145 icon_->SetImage(shadow); 146 return; 147 } 148 149 icon_->SetImage(resized); 150 } 151 152 void AppListItemView::SetUIState(UIState state) { 153 if (ui_state_ == state) 154 return; 155 156 ui_state_ = state; 157 158 switch (ui_state_) { 159 case UI_STATE_NORMAL: 160 title_->SetVisible(!is_installing_); 161 progress_bar_->SetVisible(is_installing_); 162 break; 163 case UI_STATE_DRAGGING: 164 title_->SetVisible(false); 165 progress_bar_->SetVisible(false); 166 break; 167 case UI_STATE_DROPPING_IN_FOLDER: 168 break; 169 } 170 #if !defined(OS_WIN) 171 ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator()); 172 switch (ui_state_) { 173 case UI_STATE_NORMAL: 174 layer()->SetTransform(gfx::Transform()); 175 break; 176 case UI_STATE_DRAGGING: { 177 const gfx::Rect bounds(layer()->bounds().size()); 178 layer()->SetTransform(gfx::GetScaleTransform( 179 bounds.CenterPoint(), 180 kDraggingIconScale)); 181 break; 182 } 183 case UI_STATE_DROPPING_IN_FOLDER: 184 break; 185 } 186 #endif // !OS_WIN 187 188 SchedulePaint(); 189 } 190 191 void AppListItemView::SetTouchDragging(bool touch_dragging) { 192 if (touch_dragging_ == touch_dragging) 193 return; 194 195 touch_dragging_ = touch_dragging; 196 SetUIState(touch_dragging_ ? UI_STATE_DRAGGING : UI_STATE_NORMAL); 197 } 198 199 void AppListItemView::OnMouseDragTimer() { 200 DCHECK(apps_grid_view_->IsDraggedView(this)); 201 SetUIState(UI_STATE_DRAGGING); 202 } 203 204 void AppListItemView::SetTitleSubpixelAA() { 205 // TODO(tapted): Enable AA for folders as well, taking care to play nice with 206 // the folder bubble animation. 207 bool enable_aa = !is_in_folder_ && ui_state_ == UI_STATE_NORMAL && 208 !is_highlighted_ && !apps_grid_view_->IsSelectedView(this) && 209 !apps_grid_view_->IsAnimatingView(this); 210 211 bool currently_enabled = title_->background() != NULL; 212 if (currently_enabled == enable_aa) 213 return; 214 215 if (enable_aa) { 216 title_->SetBackgroundColor(app_list::kContentsBackgroundColor); 217 title_->set_background(views::Background::CreateSolidBackground( 218 app_list::kContentsBackgroundColor)); 219 } else { 220 // In other cases, keep the background transparent to ensure correct 221 // interactions with animations. This will temporarily disable subpixel AA. 222 title_->SetBackgroundColor(0); 223 title_->set_background(NULL); 224 } 225 title_->Invalidate(); 226 title_->SchedulePaint(); 227 } 228 229 void AppListItemView::Prerender() { 230 title_->PaintToBackingImage(); 231 } 232 233 void AppListItemView::CancelContextMenu() { 234 if (context_menu_runner_) 235 context_menu_runner_->Cancel(); 236 } 237 238 gfx::ImageSkia AppListItemView::GetDragImage() { 239 return icon_->GetImage(); 240 } 241 242 void AppListItemView::OnDragEnded() { 243 mouse_drag_timer_.Stop(); 244 SetUIState(UI_STATE_NORMAL); 245 } 246 247 gfx::Point AppListItemView::GetDragImageOffset() { 248 gfx::Point image = icon_->GetImageBounds().origin(); 249 return gfx::Point(icon_->x() + image.x(), icon_->y() + image.y()); 250 } 251 252 void AppListItemView::SetAsAttemptedFolderTarget(bool is_target_folder) { 253 if (is_target_folder) 254 SetUIState(UI_STATE_DROPPING_IN_FOLDER); 255 else 256 SetUIState(UI_STATE_NORMAL); 257 } 258 259 void AppListItemView::SetItemName(const base::string16& display_name, 260 const base::string16& full_name) { 261 title_->SetText(display_name); 262 title_->Invalidate(); 263 264 title_->SetTooltipText(display_name == full_name ? base::string16() 265 : full_name); 266 267 // Use full name for accessibility. 268 SetAccessibleName( 269 is_folder_ ? l10n_util::GetStringFUTF16( 270 IDS_APP_LIST_FOLDER_BUTTON_ACCESSIBILE_NAME, full_name) 271 : full_name); 272 Layout(); 273 } 274 275 void AppListItemView::SetItemIsHighlighted(bool is_highlighted) { 276 is_highlighted_ = is_highlighted; 277 apps_grid_view_->EnsureViewVisible(this); 278 SchedulePaint(); 279 } 280 281 void AppListItemView::SetItemIsInstalling(bool is_installing) { 282 is_installing_ = is_installing; 283 if (is_installing_) 284 apps_grid_view_->EnsureViewVisible(this); 285 286 if (ui_state_ == UI_STATE_NORMAL) { 287 title_->SetVisible(!is_installing); 288 progress_bar_->SetVisible(is_installing); 289 } 290 SchedulePaint(); 291 } 292 293 void AppListItemView::SetItemPercentDownloaded(int percent_downloaded) { 294 // A percent_downloaded() of -1 can mean it's not known how much percent is 295 // completed, or the download hasn't been marked complete, as is the case 296 // while an extension is being installed after being downloaded. 297 if (percent_downloaded == -1) 298 return; 299 progress_bar_->SetValue(percent_downloaded / 100.0); 300 } 301 302 const char* AppListItemView::GetClassName() const { 303 return kViewClassName; 304 } 305 306 void AppListItemView::Layout() { 307 gfx::Rect rect(GetContentsBounds()); 308 309 const int left_right_padding = 310 title_->font_list().GetExpectedTextWidth(kLeftRightPaddingChars); 311 rect.Inset(left_right_padding, kTopPadding, left_right_padding, 0); 312 const int y = rect.y(); 313 314 icon_->SetBoundsRect(GetIconBoundsForTargetViewBounds(GetContentsBounds())); 315 const gfx::Size title_size = title_->GetPreferredSize(); 316 gfx::Rect title_bounds(rect.x() + (rect.width() - title_size.width()) / 2, 317 y + kGridIconDimension + kIconTitleSpacing, 318 title_size.width(), 319 title_size.height()); 320 title_bounds.Intersect(rect); 321 title_->SetBoundsRect(title_bounds); 322 323 gfx::Rect progress_bar_bounds(progress_bar_->GetPreferredSize()); 324 progress_bar_bounds.set_x(GetContentsBounds().x() + 325 kProgressBarHorizontalPadding); 326 progress_bar_bounds.set_y(title_bounds.y()); 327 progress_bar_->SetBoundsRect(progress_bar_bounds); 328 } 329 330 void AppListItemView::SchedulePaintInRect(const gfx::Rect& r) { 331 SetTitleSubpixelAA(); 332 views::CustomButton::SchedulePaintInRect(r); 333 } 334 335 void AppListItemView::OnPaint(gfx::Canvas* canvas) { 336 if (apps_grid_view_->IsDraggedView(this)) 337 return; 338 339 gfx::Rect rect(GetContentsBounds()); 340 if (is_highlighted_ && !is_installing_) { 341 canvas->FillRect(rect, kHighlightedColor); 342 return; 343 } 344 if (apps_grid_view_->IsSelectedView(this)) 345 canvas->FillRect(rect, kSelectedColor); 346 347 if (ui_state_ == UI_STATE_DROPPING_IN_FOLDER) { 348 DCHECK(apps_grid_view_->model()->folders_enabled()); 349 350 // Draw folder dropping preview circle. 351 gfx::Point center = gfx::Point(icon_->x() + icon_->size().width() / 2, 352 icon_->y() + icon_->size().height() / 2); 353 SkPaint paint; 354 paint.setStyle(SkPaint::kFill_Style); 355 paint.setAntiAlias(true); 356 paint.setColor(kFolderBubbleColor); 357 canvas->DrawCircle(center, kFolderPreviewRadius, paint); 358 } 359 } 360 361 void AppListItemView::ShowContextMenuForView(views::View* source, 362 const gfx::Point& point, 363 ui::MenuSourceType source_type) { 364 ui::MenuModel* menu_model = 365 item_weak_ ? item_weak_->GetContextMenuModel() : NULL; 366 if (!menu_model) 367 return; 368 369 context_menu_runner_.reset( 370 new views::MenuRunner(menu_model, views::MenuRunner::HAS_MNEMONICS)); 371 if (context_menu_runner_->RunMenuAt(GetWidget(), 372 NULL, 373 gfx::Rect(point, gfx::Size()), 374 views::MENU_ANCHOR_TOPLEFT, 375 source_type) == 376 views::MenuRunner::MENU_DELETED) { 377 return; 378 } 379 } 380 381 void AppListItemView::StateChanged() { 382 const bool is_folder_ui_enabled = apps_grid_view_->model()->folders_enabled(); 383 if (is_folder_ui_enabled) 384 apps_grid_view_->ClearAnySelectedView(); 385 386 if (state() == STATE_HOVERED || state() == STATE_PRESSED) { 387 if (!is_folder_ui_enabled) 388 apps_grid_view_->SetSelectedView(this); 389 title_->SetEnabledColor(kGridTitleHoverColor); 390 } else { 391 if (!is_folder_ui_enabled) 392 apps_grid_view_->ClearSelectedView(this); 393 is_highlighted_ = false; 394 if (item_weak_) 395 item_weak_->SetHighlighted(false); 396 title_->SetEnabledColor(kGridTitleColor); 397 } 398 title_->Invalidate(); 399 } 400 401 bool AppListItemView::ShouldEnterPushedState(const ui::Event& event) { 402 // Don't enter pushed state for ET_GESTURE_TAP_DOWN so that hover gray 403 // background does not show up during scroll. 404 if (event.type() == ui::ET_GESTURE_TAP_DOWN) 405 return false; 406 407 return views::CustomButton::ShouldEnterPushedState(event); 408 } 409 410 bool AppListItemView::OnMousePressed(const ui::MouseEvent& event) { 411 CustomButton::OnMousePressed(event); 412 413 if (!ShouldEnterPushedState(event)) 414 return true; 415 416 apps_grid_view_->InitiateDrag(this, AppsGridView::MOUSE, event); 417 418 if (apps_grid_view_->IsDraggedView(this)) { 419 mouse_drag_timer_.Start(FROM_HERE, 420 base::TimeDelta::FromMilliseconds(kMouseDragUIDelayInMs), 421 this, &AppListItemView::OnMouseDragTimer); 422 } 423 return true; 424 } 425 426 bool AppListItemView::OnKeyPressed(const ui::KeyEvent& event) { 427 // Disable space key to press the button. The keyboard events received 428 // by this view are forwarded from a Textfield (SearchBoxView) and key 429 // released events are not forwarded. This leaves the button in pressed 430 // state. 431 if (event.key_code() == ui::VKEY_SPACE) 432 return false; 433 434 return CustomButton::OnKeyPressed(event); 435 } 436 437 void AppListItemView::OnMouseReleased(const ui::MouseEvent& event) { 438 CustomButton::OnMouseReleased(event); 439 apps_grid_view_->EndDrag(false); 440 } 441 442 void AppListItemView::OnMouseCaptureLost() { 443 // We don't cancel the dag on mouse capture lost for windows as entering a 444 // synchronous drag causes mouse capture to be lost and pressing escape 445 // dismisses the app list anyway. 446 #if !defined(OS_WIN) 447 CustomButton::OnMouseCaptureLost(); 448 apps_grid_view_->EndDrag(true); 449 #endif 450 } 451 452 bool AppListItemView::OnMouseDragged(const ui::MouseEvent& event) { 453 CustomButton::OnMouseDragged(event); 454 if (apps_grid_view_->IsDraggedView(this)) { 455 // If the drag is no longer happening, it could be because this item 456 // got removed, in which case this item has been destroyed. So, bail out 457 // now as there will be nothing else to do anyway as 458 // apps_grid_view_->dragging() will be false. 459 if (!apps_grid_view_->UpdateDragFromItem(AppsGridView::MOUSE, event)) 460 return true; 461 } 462 463 // Shows dragging UI when it's confirmed without waiting for the timer. 464 if (ui_state_ != UI_STATE_DRAGGING && 465 apps_grid_view_->dragging() && 466 apps_grid_view_->IsDraggedView(this)) { 467 mouse_drag_timer_.Stop(); 468 SetUIState(UI_STATE_DRAGGING); 469 } 470 return true; 471 } 472 473 void AppListItemView::OnGestureEvent(ui::GestureEvent* event) { 474 switch (event->type()) { 475 case ui::ET_GESTURE_SCROLL_BEGIN: 476 if (touch_dragging_) { 477 apps_grid_view_->InitiateDrag(this, AppsGridView::TOUCH, *event); 478 event->SetHandled(); 479 } 480 break; 481 case ui::ET_GESTURE_SCROLL_UPDATE: 482 if (touch_dragging_ && apps_grid_view_->IsDraggedView(this)) { 483 apps_grid_view_->UpdateDragFromItem(AppsGridView::TOUCH, *event); 484 event->SetHandled(); 485 } 486 break; 487 case ui::ET_GESTURE_SCROLL_END: 488 case ui::ET_SCROLL_FLING_START: 489 if (touch_dragging_) { 490 SetTouchDragging(false); 491 apps_grid_view_->EndDrag(false); 492 event->SetHandled(); 493 } 494 break; 495 case ui::ET_GESTURE_LONG_PRESS: 496 if (!apps_grid_view_->has_dragged_view()) 497 SetTouchDragging(true); 498 event->SetHandled(); 499 break; 500 case ui::ET_GESTURE_LONG_TAP: 501 case ui::ET_GESTURE_END: 502 if (touch_dragging_) 503 SetTouchDragging(false); 504 break; 505 default: 506 break; 507 } 508 if (!event->handled()) 509 CustomButton::OnGestureEvent(event); 510 } 511 512 void AppListItemView::OnSyncDragEnd() { 513 SetUIState(UI_STATE_NORMAL); 514 } 515 516 const gfx::Rect& AppListItemView::GetIconBounds() const { 517 return icon_->bounds(); 518 } 519 520 void AppListItemView::SetDragUIState() { 521 SetUIState(UI_STATE_DRAGGING); 522 } 523 524 gfx::Rect AppListItemView::GetIconBoundsForTargetViewBounds( 525 const gfx::Rect& target_bounds) { 526 gfx::Rect rect(target_bounds); 527 528 const int left_right_padding = 529 title_->font_list().GetExpectedTextWidth(kLeftRightPaddingChars); 530 rect.Inset(left_right_padding, kTopPadding, left_right_padding, 0); 531 532 gfx::Rect icon_bounds(rect.x(), rect.y(), rect.width(), kGridIconDimension); 533 icon_bounds.Inset(gfx::ShadowValue::GetMargin(GetIconShadows())); 534 return icon_bounds; 535 } 536 537 void AppListItemView::ItemIconChanged() { 538 SetIcon(item_weak_->icon(), item_weak_->has_shadow()); 539 } 540 541 void AppListItemView::ItemNameChanged() { 542 SetItemName(base::UTF8ToUTF16(item_weak_->GetDisplayName()), 543 base::UTF8ToUTF16(item_weak_->name())); 544 } 545 546 void AppListItemView::ItemHighlightedChanged() { 547 SetItemIsHighlighted(item_weak_->highlighted()); 548 } 549 550 void AppListItemView::ItemIsInstallingChanged() { 551 SetItemIsInstalling(item_weak_->is_installing()); 552 } 553 554 void AppListItemView::ItemPercentDownloadedChanged() { 555 SetItemPercentDownloaded(item_weak_->percent_downloaded()); 556 } 557 558 void AppListItemView::ItemBeingDestroyed() { 559 DCHECK(item_weak_); 560 item_weak_->RemoveObserver(this); 561 item_weak_ = NULL; 562 } 563 564 } // namespace app_list 565