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