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
36namespace app_list {
37
38namespace {
39
40const int kTopPadding = 20;
41const int kIconTitleSpacing = 7;
42const int kProgressBarHorizontalPadding = 12;
43
44// Radius of the folder dropping preview circle.
45const int kFolderPreviewRadius = 40;
46
47const int kLeftRightPaddingChars = 1;
48
49// Scale to transform the icon when a drag starts.
50const float kDraggingIconScale = 1.5f;
51
52// Delay in milliseconds of when the dragging UI should be shown for mouse drag.
53const int kMouseDragUIDelayInMs = 200;
54
55const 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
64gfx::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
81const char AppListItemView::kViewClassName[] = "ui/app_list/AppListItemView";
82
83AppListItemView::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
126AppListItemView::~AppListItemView() {
127  if (item_weak_)
128    item_weak_->RemoveObserver(this);
129}
130
131void 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
152void 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
191void 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
199void AppListItemView::OnMouseDragTimer() {
200  DCHECK(apps_grid_view_->IsDraggedView(this));
201  SetUIState(UI_STATE_DRAGGING);
202}
203
204void 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
229void AppListItemView::Prerender() {
230  title_->PaintToBackingImage();
231}
232
233void AppListItemView::CancelContextMenu() {
234  if (context_menu_runner_)
235    context_menu_runner_->Cancel();
236}
237
238gfx::ImageSkia AppListItemView::GetDragImage() {
239  return icon_->GetImage();
240}
241
242void AppListItemView::OnDragEnded() {
243  mouse_drag_timer_.Stop();
244  SetUIState(UI_STATE_NORMAL);
245}
246
247gfx::Point AppListItemView::GetDragImageOffset() {
248  gfx::Point image = icon_->GetImageBounds().origin();
249  return gfx::Point(icon_->x() + image.x(), icon_->y() + image.y());
250}
251
252void 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
259void 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
275void AppListItemView::SetItemIsHighlighted(bool is_highlighted) {
276  is_highlighted_ = is_highlighted;
277  apps_grid_view_->EnsureViewVisible(this);
278  SchedulePaint();
279}
280
281void 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
293void 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
302const char* AppListItemView::GetClassName() const {
303  return kViewClassName;
304}
305
306void 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
330void AppListItemView::SchedulePaintInRect(const gfx::Rect& r) {
331  SetTitleSubpixelAA();
332  views::CustomButton::SchedulePaintInRect(r);
333}
334
335void 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
361void 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
381void 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
401bool 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
410bool 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
426bool 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
437void AppListItemView::OnMouseReleased(const ui::MouseEvent& event) {
438  CustomButton::OnMouseReleased(event);
439  apps_grid_view_->EndDrag(false);
440}
441
442void 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
452bool 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
473void 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
512void AppListItemView::OnSyncDragEnd() {
513  SetUIState(UI_STATE_NORMAL);
514}
515
516const gfx::Rect& AppListItemView::GetIconBounds() const {
517  return icon_->bounds();
518}
519
520void AppListItemView::SetDragUIState() {
521  SetUIState(UI_STATE_DRAGGING);
522}
523
524gfx::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
537void AppListItemView::ItemIconChanged() {
538  SetIcon(item_weak_->icon(), item_weak_->has_shadow());
539}
540
541void AppListItemView::ItemNameChanged() {
542  SetItemName(base::UTF8ToUTF16(item_weak_->GetDisplayName()),
543              base::UTF8ToUTF16(item_weak_->name()));
544}
545
546void AppListItemView::ItemHighlightedChanged() {
547  SetItemIsHighlighted(item_weak_->highlighted());
548}
549
550void AppListItemView::ItemIsInstallingChanged() {
551  SetItemIsInstalling(item_weak_->is_installing());
552}
553
554void AppListItemView::ItemPercentDownloadedChanged() {
555  SetItemPercentDownloaded(item_weak_->percent_downloaded());
556}
557
558void AppListItemView::ItemBeingDestroyed() {
559  DCHECK(item_weak_);
560  item_weak_->RemoveObserver(this);
561  item_weak_ = NULL;
562}
563
564}  // namespace app_list
565