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/profiler/scoped_tracker.h"
10 #include "base/strings/utf_string_conversions.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/base/ui_base_switches_util.h"
22 #include "ui/compositor/layer.h"
23 #include "ui/compositor/scoped_layer_animation_settings.h"
24 #include "ui/gfx/animation/throb_animation.h"
25 #include "ui/gfx/canvas.h"
26 #include "ui/gfx/font_list.h"
27 #include "ui/gfx/geometry/point.h"
28 #include "ui/gfx/geometry/vector2d.h"
29 #include "ui/gfx/image/image_skia_operations.h"
30 #include "ui/gfx/shadow_value.h"
31 #include "ui/gfx/transform_util.h"
32 #include "ui/strings/grit/ui_strings.h"
33 #include "ui/views/background.h"
34 #include "ui/views/controls/image_view.h"
35 #include "ui/views/controls/label.h"
36 #include "ui/views/controls/menu/menu_runner.h"
37 #include "ui/views/drag_controller.h"
43 const int kTopPadding
= 18;
44 const int kIconTitleSpacing
= 6;
45 const int kProgressBarHorizontalPadding
= 12;
47 // Radius of the folder dropping preview circle.
48 const int kFolderPreviewRadius
= 40;
50 const int kLeftRightPaddingChars
= 1;
52 // Scale to transform the icon when a drag starts.
53 const float kDraggingIconScale
= 1.5f
;
55 // Delay in milliseconds of when the dragging UI should be shown for mouse drag.
56 const int kMouseDragUIDelayInMs
= 200;
58 const gfx::ShadowValues
& GetIconShadows() {
59 CR_DEFINE_STATIC_LOCAL(const gfx::ShadowValues
, icon_shadows
,
60 (1, gfx::ShadowValue(gfx::Vector2d(0, 2), 2,
61 SkColorSetARGB(0x24, 0, 0, 0))));
65 gfx::FontList
GetFontList() {
66 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
67 const gfx::FontList
& font_list
= rb
.GetFontList(kItemTextFontStyle
);
68 // The font is different on each platform. The font size is adjusted on some
69 // platforms to keep a consistent look.
70 #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
71 // Reducing the font size by 2 makes it the same as the Windows font size.
72 const int kFontSizeDelta
= -2;
73 return font_list
.DeriveWithSizeDelta(kFontSizeDelta
);
82 const char AppListItemView::kViewClassName
[] = "ui/app_list/AppListItemView";
84 AppListItemView::AppListItemView(AppsGridView
* apps_grid_view
,
86 : CustomButton(apps_grid_view
),
87 is_folder_(item
->GetItemType() == AppListFolderItem::kItemType
),
88 is_in_folder_(item
->IsInFolder()),
90 apps_grid_view_(apps_grid_view
),
91 icon_(new views::ImageView
),
92 title_(new CachedLabel
),
93 progress_bar_(new ProgressBarView
),
94 ui_state_(UI_STATE_NORMAL
),
95 touch_dragging_(false),
96 is_installing_(false),
97 is_highlighted_(false) {
98 icon_
->set_interactive(false);
100 title_
->SetBackgroundColor(0);
101 title_
->SetAutoColorReadabilityEnabled(false);
102 title_
->SetEnabledColor(kGridTitleColor
);
103 title_
->SetHandlesTooltips(false);
105 static const gfx::FontList font_list
= GetFontList();
106 title_
->SetFontList(font_list
);
107 title_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
108 title_
->Invalidate();
109 SetTitleSubpixelAA();
112 AddChildView(title_
);
113 AddChildView(progress_bar_
);
115 SetIcon(item
->icon(), item
->has_shadow());
116 SetItemName(base::UTF8ToUTF16(item
->GetDisplayName()),
117 base::UTF8ToUTF16(item
->name()));
118 SetItemIsInstalling(item
->is_installing());
119 SetItemIsHighlighted(item
->highlighted());
120 item
->AddObserver(this);
122 set_context_menu_controller(this);
123 set_request_focus_on_press(false);
125 SetAnimationDuration(0);
128 AppListItemView::~AppListItemView() {
130 item_weak_
->RemoveObserver(this);
133 void AppListItemView::SetIcon(const gfx::ImageSkia
& icon
, bool has_shadow
) {
134 // Clear icon and bail out if item icon is empty.
136 icon_
->SetImage(NULL
);
140 gfx::ImageSkia
resized(gfx::ImageSkiaOperations::CreateResizedImage(
142 skia::ImageOperations::RESIZE_BEST
,
143 gfx::Size(kGridIconDimension
, kGridIconDimension
)));
145 gfx::ImageSkia
shadow(gfx::ImageSkiaOperations::CreateImageWithDropShadow(
146 resized
, GetIconShadows()));
147 icon_
->SetImage(shadow
);
151 icon_
->SetImage(resized
);
154 void AppListItemView::SetUIState(UIState state
) {
155 if (ui_state_
== state
)
161 case UI_STATE_NORMAL
:
162 title_
->SetVisible(!is_installing_
);
163 progress_bar_
->SetVisible(is_installing_
);
165 case UI_STATE_DRAGGING
:
166 title_
->SetVisible(false);
167 progress_bar_
->SetVisible(false);
169 case UI_STATE_DROPPING_IN_FOLDER
:
173 ui::ScopedLayerAnimationSettings
settings(layer()->GetAnimator());
175 case UI_STATE_NORMAL
:
176 layer()->SetTransform(gfx::Transform());
178 case UI_STATE_DRAGGING
: {
179 const gfx::Rect
bounds(layer()->bounds().size());
180 layer()->SetTransform(gfx::GetScaleTransform(
181 bounds
.CenterPoint(),
182 kDraggingIconScale
));
185 case UI_STATE_DROPPING_IN_FOLDER
:
190 SetTitleSubpixelAA();
194 void AppListItemView::SetTouchDragging(bool touch_dragging
) {
195 if (touch_dragging_
== touch_dragging
)
198 touch_dragging_
= touch_dragging
;
199 SetState(STATE_NORMAL
);
200 SetUIState(touch_dragging_
? UI_STATE_DRAGGING
: UI_STATE_NORMAL
);
203 void AppListItemView::OnMouseDragTimer() {
204 DCHECK(apps_grid_view_
->IsDraggedView(this));
205 SetUIState(UI_STATE_DRAGGING
);
208 void AppListItemView::Prerender() {
209 title_
->PaintToBackingImage();
212 void AppListItemView::CancelContextMenu() {
213 if (context_menu_runner_
)
214 context_menu_runner_
->Cancel();
217 gfx::ImageSkia
AppListItemView::GetDragImage() {
218 return icon_
->GetImage();
221 void AppListItemView::OnDragEnded() {
222 mouse_drag_timer_
.Stop();
223 SetUIState(UI_STATE_NORMAL
);
226 gfx::Point
AppListItemView::GetDragImageOffset() {
227 gfx::Point image
= icon_
->GetImageBounds().origin();
228 return gfx::Point(icon_
->x() + image
.x(), icon_
->y() + image
.y());
231 void AppListItemView::SetAsAttemptedFolderTarget(bool is_target_folder
) {
232 if (is_target_folder
)
233 SetUIState(UI_STATE_DROPPING_IN_FOLDER
);
235 SetUIState(UI_STATE_NORMAL
);
238 void AppListItemView::SetItemName(const base::string16
& display_name
,
239 const base::string16
& full_name
) {
240 title_
->SetText(display_name
);
241 title_
->Invalidate();
243 tooltip_text_
= display_name
== full_name
? base::string16() : full_name
;
245 // Use full name for accessibility.
247 is_folder_
? l10n_util::GetStringFUTF16(
248 IDS_APP_LIST_FOLDER_BUTTON_ACCESSIBILE_NAME
, full_name
)
253 void AppListItemView::SetItemIsHighlighted(bool is_highlighted
) {
254 is_highlighted_
= is_highlighted
;
255 SetTitleSubpixelAA();
259 void AppListItemView::SetItemIsInstalling(bool is_installing
) {
260 is_installing_
= is_installing
;
261 if (ui_state_
== UI_STATE_NORMAL
) {
262 title_
->SetVisible(!is_installing
);
263 progress_bar_
->SetVisible(is_installing
);
265 SetTitleSubpixelAA();
269 void AppListItemView::SetItemPercentDownloaded(int percent_downloaded
) {
270 // A percent_downloaded() of -1 can mean it's not known how much percent is
271 // completed, or the download hasn't been marked complete, as is the case
272 // while an extension is being installed after being downloaded.
273 if (percent_downloaded
== -1)
275 progress_bar_
->SetValue(percent_downloaded
/ 100.0);
278 const char* AppListItemView::GetClassName() const {
279 return kViewClassName
;
282 void AppListItemView::Layout() {
283 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
284 tracked_objects::ScopedTracker
tracking_profile1(
285 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 AppListItemView::Layout1"));
287 gfx::Rect
rect(GetContentsBounds());
289 const int left_right_padding
=
290 title_
->font_list().GetExpectedTextWidth(kLeftRightPaddingChars
);
291 rect
.Inset(left_right_padding
, kTopPadding
, left_right_padding
, 0);
292 const int y
= rect
.y();
294 icon_
->SetBoundsRect(GetIconBoundsForTargetViewBounds(GetContentsBounds()));
296 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
297 tracked_objects::ScopedTracker
tracking_profile2(
298 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 AppListItemView::Layout2"));
300 const gfx::Size title_size
= title_
->GetPreferredSize();
302 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
303 tracked_objects::ScopedTracker
tracking_profile3(
304 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 AppListItemView::Layout3"));
306 gfx::Rect
title_bounds(rect
.x() + (rect
.width() - title_size
.width()) / 2,
307 y
+ kGridIconDimension
+ kIconTitleSpacing
,
309 title_size
.height());
310 title_bounds
.Intersect(rect
);
311 title_
->SetBoundsRect(title_bounds
);
312 SetTitleSubpixelAA();
314 gfx::Rect
progress_bar_bounds(progress_bar_
->GetPreferredSize());
315 progress_bar_bounds
.set_x(GetContentsBounds().x() +
316 kProgressBarHorizontalPadding
);
317 progress_bar_bounds
.set_y(title_bounds
.y());
318 progress_bar_
->SetBoundsRect(progress_bar_bounds
);
321 void AppListItemView::OnPaint(gfx::Canvas
* canvas
) {
322 if (apps_grid_view_
->IsDraggedView(this))
325 gfx::Rect
rect(GetContentsBounds());
326 if (apps_grid_view_
->IsSelectedView(this)) {
327 canvas
->FillRect(rect
, kSelectedColor
);
328 } else if (is_highlighted_
&& !is_installing_
) {
329 canvas
->FillRect(rect
, kHighlightedColor
);
333 if (ui_state_
== UI_STATE_DROPPING_IN_FOLDER
) {
334 DCHECK(apps_grid_view_
->model()->folders_enabled());
336 // Draw folder dropping preview circle.
337 gfx::Point center
= gfx::Point(icon_
->x() + icon_
->size().width() / 2,
338 icon_
->y() + icon_
->size().height() / 2);
340 paint
.setStyle(SkPaint::kFill_Style
);
341 paint
.setAntiAlias(true);
342 paint
.setColor(kFolderBubbleColor
);
343 canvas
->DrawCircle(center
, kFolderPreviewRadius
, paint
);
347 void AppListItemView::ShowContextMenuForView(views::View
* source
,
348 const gfx::Point
& point
,
349 ui::MenuSourceType source_type
) {
350 ui::MenuModel
* menu_model
=
351 item_weak_
? item_weak_
->GetContextMenuModel() : NULL
;
355 if (!apps_grid_view_
->IsSelectedView(this))
356 apps_grid_view_
->ClearAnySelectedView();
357 context_menu_runner_
.reset(
358 new views::MenuRunner(menu_model
, views::MenuRunner::HAS_MNEMONICS
));
359 if (context_menu_runner_
->RunMenuAt(GetWidget(),
361 gfx::Rect(point
, gfx::Size()),
362 views::MENU_ANCHOR_TOPLEFT
,
364 views::MenuRunner::MENU_DELETED
) {
369 void AppListItemView::StateChanged() {
370 if (state() == STATE_HOVERED
|| state() == STATE_PRESSED
) {
371 // Show the hover/tap highlight: for tap, lighter highlight replaces darker
372 // keyboard selection; for mouse hover, keyboard selection takes precedence.
373 if (!apps_grid_view_
->IsSelectedView(this) || state() == STATE_PRESSED
)
374 SetItemIsHighlighted(true);
375 title_
->SetEnabledColor(kGridTitleHoverColor
);
377 SetItemIsHighlighted(false);
379 item_weak_
->set_highlighted(false);
380 title_
->SetEnabledColor(kGridTitleColor
);
382 SetTitleSubpixelAA();
385 bool AppListItemView::ShouldEnterPushedState(const ui::Event
& event
) {
386 // Don't enter pushed state for ET_GESTURE_TAP_DOWN so that hover gray
387 // background does not show up during scroll.
388 if (event
.type() == ui::ET_GESTURE_TAP_DOWN
)
391 return views::CustomButton::ShouldEnterPushedState(event
);
394 bool AppListItemView::OnMousePressed(const ui::MouseEvent
& event
) {
395 CustomButton::OnMousePressed(event
);
397 if (!ShouldEnterPushedState(event
))
400 apps_grid_view_
->InitiateDrag(this, AppsGridView::MOUSE
, event
);
402 if (apps_grid_view_
->IsDraggedView(this)) {
403 mouse_drag_timer_
.Start(FROM_HERE
,
404 base::TimeDelta::FromMilliseconds(kMouseDragUIDelayInMs
),
405 this, &AppListItemView::OnMouseDragTimer
);
410 bool AppListItemView::OnKeyPressed(const ui::KeyEvent
& event
) {
411 // Disable space key to press the button. The keyboard events received
412 // by this view are forwarded from a Textfield (SearchBoxView) and key
413 // released events are not forwarded. This leaves the button in pressed
415 if (event
.key_code() == ui::VKEY_SPACE
)
418 return CustomButton::OnKeyPressed(event
);
421 void AppListItemView::OnMouseReleased(const ui::MouseEvent
& event
) {
422 CustomButton::OnMouseReleased(event
);
423 apps_grid_view_
->EndDrag(false);
426 void AppListItemView::OnMouseCaptureLost() {
427 // We don't cancel the dag on mouse capture lost for windows as entering a
428 // synchronous drag causes mouse capture to be lost and pressing escape
429 // dismisses the app list anyway.
431 CustomButton::OnMouseCaptureLost();
432 apps_grid_view_
->EndDrag(true);
436 bool AppListItemView::OnMouseDragged(const ui::MouseEvent
& event
) {
437 CustomButton::OnMouseDragged(event
);
438 if (apps_grid_view_
->IsDraggedView(this)) {
439 // If the drag is no longer happening, it could be because this item
440 // got removed, in which case this item has been destroyed. So, bail out
441 // now as there will be nothing else to do anyway as
442 // apps_grid_view_->dragging() will be false.
443 if (!apps_grid_view_
->UpdateDragFromItem(AppsGridView::MOUSE
, event
))
447 if (!apps_grid_view_
->IsSelectedView(this))
448 apps_grid_view_
->ClearAnySelectedView();
450 // Shows dragging UI when it's confirmed without waiting for the timer.
451 if (ui_state_
!= UI_STATE_DRAGGING
&&
452 apps_grid_view_
->dragging() &&
453 apps_grid_view_
->IsDraggedView(this)) {
454 mouse_drag_timer_
.Stop();
455 SetUIState(UI_STATE_DRAGGING
);
460 void AppListItemView::OnGestureEvent(ui::GestureEvent
* event
) {
461 switch (event
->type()) {
462 case ui::ET_GESTURE_SCROLL_BEGIN
:
463 if (touch_dragging_
) {
464 apps_grid_view_
->InitiateDrag(this, AppsGridView::TOUCH
, *event
);
468 case ui::ET_GESTURE_SCROLL_UPDATE
:
469 if (touch_dragging_
&& apps_grid_view_
->IsDraggedView(this)) {
470 apps_grid_view_
->UpdateDragFromItem(AppsGridView::TOUCH
, *event
);
474 case ui::ET_GESTURE_SCROLL_END
:
475 case ui::ET_SCROLL_FLING_START
:
476 if (touch_dragging_
) {
477 SetTouchDragging(false);
478 apps_grid_view_
->EndDrag(false);
482 case ui::ET_GESTURE_TAP_DOWN
:
483 if (switches::IsTouchFeedbackEnabled() && state_
!= STATE_DISABLED
) {
484 SetState(STATE_PRESSED
);
488 case ui::ET_GESTURE_TAP
:
489 case ui::ET_GESTURE_TAP_CANCEL
:
490 if (switches::IsTouchFeedbackEnabled() && state_
!= STATE_DISABLED
)
491 SetState(STATE_NORMAL
);
493 case ui::ET_GESTURE_LONG_PRESS
:
494 if (!apps_grid_view_
->has_dragged_view())
495 SetTouchDragging(true);
498 case ui::ET_GESTURE_LONG_TAP
:
499 case ui::ET_GESTURE_END
:
501 SetTouchDragging(false);
506 if (!event
->handled())
507 CustomButton::OnGestureEvent(event
);
510 bool AppListItemView::GetTooltipText(const gfx::Point
& p
,
511 base::string16
* tooltip
) const {
512 // Use the label to generate a tooltip, so that it will consider its text
513 // truncation in making the tooltip. We do not want the label itself to have a
514 // tooltip, so we only temporarily enable it to get the tooltip text from the
515 // label, then disable it again.
516 title_
->SetHandlesTooltips(true);
517 title_
->SetTooltipText(tooltip_text_
);
518 bool handled
= title_
->GetTooltipText(p
, tooltip
);
519 title_
->SetHandlesTooltips(false);
523 void AppListItemView::OnSyncDragEnd() {
524 SetUIState(UI_STATE_NORMAL
);
527 const gfx::Rect
& AppListItemView::GetIconBounds() const {
528 return icon_
->bounds();
531 void AppListItemView::SetDragUIState() {
532 SetUIState(UI_STATE_DRAGGING
);
535 gfx::Rect
AppListItemView::GetIconBoundsForTargetViewBounds(
536 const gfx::Rect
& target_bounds
) {
537 gfx::Rect
rect(target_bounds
);
539 const int left_right_padding
=
540 title_
->font_list().GetExpectedTextWidth(kLeftRightPaddingChars
);
541 rect
.Inset(left_right_padding
, kTopPadding
, left_right_padding
, 0);
543 gfx::Rect
icon_bounds(rect
.x(), rect
.y(), rect
.width(), kGridIconDimension
);
544 icon_bounds
.Inset(gfx::ShadowValue::GetMargin(GetIconShadows()));
548 void AppListItemView::SetTitleSubpixelAA() {
549 // TODO(tapted): Enable AA for folders as well, taking care to play nice with
550 // the folder bubble animation.
551 bool enable_aa
= !is_in_folder_
&& ui_state_
== UI_STATE_NORMAL
&&
552 !is_highlighted_
&& !apps_grid_view_
->IsSelectedView(this) &&
553 !apps_grid_view_
->IsAnimatingView(this);
555 title_
->SetSubpixelRenderingEnabled(enable_aa
);
557 title_
->SetBackgroundColor(app_list::kLabelBackgroundColor
);
558 title_
->set_background(views::Background::CreateSolidBackground(
559 app_list::kLabelBackgroundColor
));
561 // In other cases, keep the background transparent to ensure correct
562 // interactions with animations. This will temporarily disable subpixel AA.
563 title_
->SetBackgroundColor(0);
564 title_
->set_background(NULL
);
566 title_
->Invalidate();
567 title_
->SchedulePaint();
570 void AppListItemView::ItemIconChanged() {
571 SetIcon(item_weak_
->icon(), item_weak_
->has_shadow());
574 void AppListItemView::ItemNameChanged() {
575 SetItemName(base::UTF8ToUTF16(item_weak_
->GetDisplayName()),
576 base::UTF8ToUTF16(item_weak_
->name()));
579 void AppListItemView::ItemIsInstallingChanged() {
580 SetItemIsInstalling(item_weak_
->is_installing());
583 void AppListItemView::ItemPercentDownloadedChanged() {
584 SetItemPercentDownloaded(item_weak_
->percent_downloaded());
587 void AppListItemView::ItemBeingDestroyed() {
589 item_weak_
->RemoveObserver(this);
593 } // namespace app_list