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