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
AppListItemView(AppsGridView * apps_grid_view,AppListItem * item)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
~AppListItemView()111 AppListItemView::~AppListItemView() {
112 item_->RemoveObserver(this);
113 }
114
SetIconSize(const gfx::Size & size)115 void AppListItemView::SetIconSize(const gfx::Size& size) {
116 if (icon_size_ == size)
117 return;
118
119 icon_size_ = size;
120 UpdateIcon();
121 }
122
UpdateIcon()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
UpdateTooltip()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
SetUIState(UIState state)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
SetTouchDragging(bool touch_dragging)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
OnMouseDragTimer()201 void AppListItemView::OnMouseDragTimer() {
202 DCHECK(apps_grid_view_->IsDraggedView(this));
203 SetUIState(UI_STATE_DRAGGING);
204 }
205
SetTitleSubpixelAA()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
Prerender()232 void AppListItemView::Prerender() {
233 title_->PaintToBackingImage();
234 }
235
CancelContextMenu()236 void AppListItemView::CancelContextMenu() {
237 if (context_menu_runner_)
238 context_menu_runner_->Cancel();
239 }
240
GetDragImage()241 gfx::ImageSkia AppListItemView::GetDragImage() {
242 return icon_->GetImage();
243 }
244
OnDragEnded()245 void AppListItemView::OnDragEnded() {
246 mouse_drag_timer_.Stop();
247 SetUIState(UI_STATE_NORMAL);
248 }
249
GetDragImageOffset()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
SetAsAttemptedFolderTarget(bool is_target_folder)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
ItemIconChanged()262 void AppListItemView::ItemIconChanged() {
263 UpdateIcon();
264 }
265
ItemNameChanged()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
ItemHighlightedChanged()279 void AppListItemView::ItemHighlightedChanged() {
280 apps_grid_view_->EnsureViewVisible(this);
281 SchedulePaint();
282 }
283
ItemIsInstallingChanged()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
ItemPercentDownloadedChanged()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
GetClassName() const301 const char* AppListItemView::GetClassName() const {
302 return kViewClassName;
303 }
304
Layout()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
SchedulePaintInRect(const gfx::Rect & r)329 void AppListItemView::SchedulePaintInRect(const gfx::Rect& r) {
330 SetTitleSubpixelAA();
331 views::CustomButton::SchedulePaintInRect(r);
332 }
333
OnPaint(gfx::Canvas * canvas)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
ShowContextMenuForView(views::View * source,const gfx::Point & point,ui::MenuSourceType source_type)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
StateChanged()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
ShouldEnterPushedState(const ui::Event & event)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
OnMousePressed(const ui::MouseEvent & event)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
OnKeyPressed(const ui::KeyEvent & event)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
OnMouseReleased(const ui::MouseEvent & event)433 void AppListItemView::OnMouseReleased(const ui::MouseEvent& event) {
434 CustomButton::OnMouseReleased(event);
435 apps_grid_view_->EndDrag(false);
436 }
437
OnMouseCaptureLost()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
OnMouseDragged(const ui::MouseEvent & event)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
OnGestureEvent(ui::GestureEvent * event)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
OnSyncDragEnd()508 void AppListItemView::OnSyncDragEnd() {
509 SetUIState(UI_STATE_NORMAL);
510 }
511
GetIconBounds() const512 const gfx::Rect& AppListItemView::GetIconBounds() const {
513 return icon_->bounds();
514 }
515
SetDragUIState()516 void AppListItemView::SetDragUIState() {
517 SetUIState(UI_STATE_DRAGGING);
518 }
519
GetIconBoundsForTargetViewBounds(const gfx::Rect & target_bounds)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