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.
5 #include "ui/app_list/views/app_list_item_view.h"
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"
40 const int kTopPadding = 20;
41 const int kIconTitleSpacing = 7;
42 const int kProgressBarHorizontalPadding = 12;
44 // Radius of the folder dropping preview circle.
45 const int kFolderPreviewRadius = 40;
47 const int kLeftRightPaddingChars = 1;
49 // Scale to transform the icon when a drag starts.
50 const float kDraggingIconScale = 1.5f;
52 // Delay in milliseconds of when the dragging UI should be shown for mouse drag.
53 const int kMouseDragUIDelayInMs = 200;
55 const gfx::ShadowValues& GetIconShadows() {
56 CR_DEFINE_STATIC_LOCAL(
57 const gfx::ShadowValues,
60 gfx::ShadowValue(gfx::Point(0, 2), 2, SkColorSetARGB(0x24, 0, 0, 0))));
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);
81 const char AppListItemView::kViewClassName[] = "ui/app_list/AppListItemView";
83 AppListItemView::AppListItemView(AppsGridView* apps_grid_view,
85 : CustomButton(apps_grid_view),
86 is_folder_(item->GetItemType() == AppListFolderItem::kItemType),
87 is_in_folder_(item->IsInFolder()),
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);
99 title_->SetBackgroundColor(0);
100 title_->SetAutoColorReadabilityEnabled(false);
101 title_->SetEnabledColor(kGridTitleColor);
103 static const gfx::FontList font_list = GetFontList();
104 title_->SetFontList(font_list);
105 title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
106 title_->Invalidate();
107 SetTitleSubpixelAA();
110 AddChildView(title_);
111 AddChildView(progress_bar_);
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);
120 set_context_menu_controller(this);
121 set_request_focus_on_press(false);
123 SetAnimationDuration(0);
126 AppListItemView::~AppListItemView() {
128 item_weak_->RemoveObserver(this);
131 void AppListItemView::SetIcon(const gfx::ImageSkia& icon, bool has_shadow) {
132 // Clear icon and bail out if item icon is empty.
134 icon_->SetImage(NULL);
138 gfx::ImageSkia resized(gfx::ImageSkiaOperations::CreateResizedImage(
140 skia::ImageOperations::RESIZE_BEST,
141 gfx::Size(kGridIconDimension, kGridIconDimension)));
143 gfx::ImageSkia shadow(gfx::ImageSkiaOperations::CreateImageWithDropShadow(
144 resized, GetIconShadows()));
145 icon_->SetImage(shadow);
149 icon_->SetImage(resized);
152 void AppListItemView::SetUIState(UIState state) {
153 if (ui_state_ == state)
159 case UI_STATE_NORMAL:
160 title_->SetVisible(!is_installing_);
161 progress_bar_->SetVisible(is_installing_);
163 case UI_STATE_DRAGGING:
164 title_->SetVisible(false);
165 progress_bar_->SetVisible(false);
167 case UI_STATE_DROPPING_IN_FOLDER:
171 ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator());
173 case UI_STATE_NORMAL:
174 layer()->SetTransform(gfx::Transform());
176 case UI_STATE_DRAGGING: {
177 const gfx::Rect bounds(layer()->bounds().size());
178 layer()->SetTransform(gfx::GetScaleTransform(
179 bounds.CenterPoint(),
180 kDraggingIconScale));
183 case UI_STATE_DROPPING_IN_FOLDER:
191 void AppListItemView::SetTouchDragging(bool touch_dragging) {
192 if (touch_dragging_ == touch_dragging)
195 touch_dragging_ = touch_dragging;
196 SetUIState(touch_dragging_ ? UI_STATE_DRAGGING : UI_STATE_NORMAL);
199 void AppListItemView::OnMouseDragTimer() {
200 DCHECK(apps_grid_view_->IsDraggedView(this));
201 SetUIState(UI_STATE_DRAGGING);
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);
211 bool currently_enabled = title_->background() != NULL;
212 if (currently_enabled == enable_aa)
216 title_->SetBackgroundColor(app_list::kContentsBackgroundColor);
217 title_->set_background(views::Background::CreateSolidBackground(
218 app_list::kContentsBackgroundColor));
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);
225 title_->Invalidate();
226 title_->SchedulePaint();
229 void AppListItemView::Prerender() {
230 title_->PaintToBackingImage();
233 void AppListItemView::CancelContextMenu() {
234 if (context_menu_runner_)
235 context_menu_runner_->Cancel();
238 gfx::ImageSkia AppListItemView::GetDragImage() {
239 return icon_->GetImage();
242 void AppListItemView::OnDragEnded() {
243 mouse_drag_timer_.Stop();
244 SetUIState(UI_STATE_NORMAL);
247 gfx::Point AppListItemView::GetDragImageOffset() {
248 gfx::Point image = icon_->GetImageBounds().origin();
249 return gfx::Point(icon_->x() + image.x(), icon_->y() + image.y());
252 void AppListItemView::SetAsAttemptedFolderTarget(bool is_target_folder) {
253 if (is_target_folder)
254 SetUIState(UI_STATE_DROPPING_IN_FOLDER);
256 SetUIState(UI_STATE_NORMAL);
259 void AppListItemView::SetItemName(const base::string16& display_name,
260 const base::string16& full_name) {
261 title_->SetText(display_name);
262 title_->Invalidate();
264 title_->SetTooltipText(display_name == full_name ? base::string16()
267 // Use full name for accessibility.
269 is_folder_ ? l10n_util::GetStringFUTF16(
270 IDS_APP_LIST_FOLDER_BUTTON_ACCESSIBILE_NAME, full_name)
275 void AppListItemView::SetItemIsHighlighted(bool is_highlighted) {
276 is_highlighted_ = is_highlighted;
277 apps_grid_view_->EnsureViewVisible(this);
281 void AppListItemView::SetItemIsInstalling(bool is_installing) {
282 is_installing_ = is_installing;
284 apps_grid_view_->EnsureViewVisible(this);
286 if (ui_state_ == UI_STATE_NORMAL) {
287 title_->SetVisible(!is_installing);
288 progress_bar_->SetVisible(is_installing);
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)
299 progress_bar_->SetValue(percent_downloaded / 100.0);
302 const char* AppListItemView::GetClassName() const {
303 return kViewClassName;
306 void AppListItemView::Layout() {
307 gfx::Rect rect(GetContentsBounds());
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();
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,
319 title_size.height());
320 title_bounds.Intersect(rect);
321 title_->SetBoundsRect(title_bounds);
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);
330 void AppListItemView::SchedulePaintInRect(const gfx::Rect& r) {
331 SetTitleSubpixelAA();
332 views::CustomButton::SchedulePaintInRect(r);
335 void AppListItemView::OnPaint(gfx::Canvas* canvas) {
336 if (apps_grid_view_->IsDraggedView(this))
339 gfx::Rect rect(GetContentsBounds());
340 if (is_highlighted_ && !is_installing_) {
341 canvas->FillRect(rect, kHighlightedColor);
344 if (apps_grid_view_->IsSelectedView(this))
345 canvas->FillRect(rect, kSelectedColor);
347 if (ui_state_ == UI_STATE_DROPPING_IN_FOLDER) {
348 DCHECK(apps_grid_view_->model()->folders_enabled());
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);
354 paint.setStyle(SkPaint::kFill_Style);
355 paint.setAntiAlias(true);
356 paint.setColor(kFolderBubbleColor);
357 canvas->DrawCircle(center, kFolderPreviewRadius, paint);
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;
369 context_menu_runner_.reset(
370 new views::MenuRunner(menu_model, views::MenuRunner::HAS_MNEMONICS));
371 if (context_menu_runner_->RunMenuAt(GetWidget(),
373 gfx::Rect(point, gfx::Size()),
374 views::MENU_ANCHOR_TOPLEFT,
376 views::MenuRunner::MENU_DELETED) {
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();
386 if (state() == STATE_HOVERED || state() == STATE_PRESSED) {
387 if (!is_folder_ui_enabled)
388 apps_grid_view_->SetSelectedView(this);
389 title_->SetEnabledColor(kGridTitleHoverColor);
391 if (!is_folder_ui_enabled)
392 apps_grid_view_->ClearSelectedView(this);
393 is_highlighted_ = false;
395 item_weak_->SetHighlighted(false);
396 title_->SetEnabledColor(kGridTitleColor);
398 title_->Invalidate();
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)
407 return views::CustomButton::ShouldEnterPushedState(event);
410 bool AppListItemView::OnMousePressed(const ui::MouseEvent& event) {
411 CustomButton::OnMousePressed(event);
413 if (!ShouldEnterPushedState(event))
416 apps_grid_view_->InitiateDrag(this, AppsGridView::MOUSE, event);
418 if (apps_grid_view_->IsDraggedView(this)) {
419 mouse_drag_timer_.Start(FROM_HERE,
420 base::TimeDelta::FromMilliseconds(kMouseDragUIDelayInMs),
421 this, &AppListItemView::OnMouseDragTimer);
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
431 if (event.key_code() == ui::VKEY_SPACE)
434 return CustomButton::OnKeyPressed(event);
437 void AppListItemView::OnMouseReleased(const ui::MouseEvent& event) {
438 CustomButton::OnMouseReleased(event);
439 apps_grid_view_->EndDrag(false);
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.
447 CustomButton::OnMouseCaptureLost();
448 apps_grid_view_->EndDrag(true);
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))
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);
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);
481 case ui::ET_GESTURE_SCROLL_UPDATE:
482 if (touch_dragging_ && apps_grid_view_->IsDraggedView(this)) {
483 apps_grid_view_->UpdateDragFromItem(AppsGridView::TOUCH, *event);
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);
495 case ui::ET_GESTURE_LONG_PRESS:
496 if (!apps_grid_view_->has_dragged_view())
497 SetTouchDragging(true);
500 case ui::ET_GESTURE_LONG_TAP:
501 case ui::ET_GESTURE_END:
503 SetTouchDragging(false);
508 if (!event->handled())
509 CustomButton::OnGestureEvent(event);
512 void AppListItemView::OnSyncDragEnd() {
513 SetUIState(UI_STATE_NORMAL);
516 const gfx::Rect& AppListItemView::GetIconBounds() const {
517 return icon_->bounds();
520 void AppListItemView::SetDragUIState() {
521 SetUIState(UI_STATE_DRAGGING);
524 gfx::Rect AppListItemView::GetIconBoundsForTargetViewBounds(
525 const gfx::Rect& target_bounds) {
526 gfx::Rect rect(target_bounds);
528 const int left_right_padding =
529 title_->font_list().GetExpectedTextWidth(kLeftRightPaddingChars);
530 rect.Inset(left_right_padding, kTopPadding, left_right_padding, 0);
532 gfx::Rect icon_bounds(rect.x(), rect.y(), rect.width(), kGridIconDimension);
533 icon_bounds.Inset(gfx::ShadowValue::GetMargin(GetIconShadows()));
537 void AppListItemView::ItemIconChanged() {
538 SetIcon(item_weak_->icon(), item_weak_->has_shadow());
541 void AppListItemView::ItemNameChanged() {
542 SetItemName(base::UTF8ToUTF16(item_weak_->GetDisplayName()),
543 base::UTF8ToUTF16(item_weak_->name()));
546 void AppListItemView::ItemHighlightedChanged() {
547 SetItemIsHighlighted(item_weak_->highlighted());
550 void AppListItemView::ItemIsInstallingChanged() {
551 SetItemIsInstalling(item_weak_->is_installing());
554 void AppListItemView::ItemPercentDownloadedChanged() {
555 SetItemPercentDownloaded(item_weak_->percent_downloaded());
558 void AppListItemView::ItemBeingDestroyed() {
560 item_weak_->RemoveObserver(this);
564 } // namespace app_list