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 "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 
     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 
    111 AppListItemView::~AppListItemView() {
    112   item_->RemoveObserver(this);
    113 }
    114 
    115 void AppListItemView::SetIconSize(const gfx::Size& size) {
    116   if (icon_size_ == size)
    117     return;
    118 
    119   icon_size_ = size;
    120   UpdateIcon();
    121 }
    122 
    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 
    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 
    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 
    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 
    201 void AppListItemView::OnMouseDragTimer() {
    202   DCHECK(apps_grid_view_->IsDraggedView(this));
    203   SetUIState(UI_STATE_DRAGGING);
    204 }
    205 
    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 
    232 void AppListItemView::Prerender() {
    233   title_->PaintToBackingImage();
    234 }
    235 
    236 void AppListItemView::CancelContextMenu() {
    237   if (context_menu_runner_)
    238     context_menu_runner_->Cancel();
    239 }
    240 
    241 gfx::ImageSkia AppListItemView::GetDragImage() {
    242   return icon_->GetImage();
    243 }
    244 
    245 void AppListItemView::OnDragEnded() {
    246   mouse_drag_timer_.Stop();
    247   SetUIState(UI_STATE_NORMAL);
    248 }
    249 
    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 
    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 
    262 void AppListItemView::ItemIconChanged() {
    263   UpdateIcon();
    264 }
    265 
    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 
    279 void AppListItemView::ItemHighlightedChanged() {
    280   apps_grid_view_->EnsureViewVisible(this);
    281   SchedulePaint();
    282 }
    283 
    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 
    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 
    301 const char* AppListItemView::GetClassName() const {
    302   return kViewClassName;
    303 }
    304 
    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 
    329 void AppListItemView::SchedulePaintInRect(const gfx::Rect& r) {
    330   SetTitleSubpixelAA();
    331   views::CustomButton::SchedulePaintInRect(r);
    332 }
    333 
    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 
    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 
    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 
    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 
    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 
    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 
    433 void AppListItemView::OnMouseReleased(const ui::MouseEvent& event) {
    434   CustomButton::OnMouseReleased(event);
    435   apps_grid_view_->EndDrag(false);
    436 }
    437 
    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 
    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 
    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 
    508 void AppListItemView::OnSyncDragEnd() {
    509   SetUIState(UI_STATE_NORMAL);
    510 }
    511 
    512 const gfx::Rect& AppListItemView::GetIconBounds() const {
    513   return icon_->bounds();
    514 }
    515 
    516 void AppListItemView::SetDragUIState() {
    517   SetUIState(UI_STATE_DRAGGING);
    518 }
    519 
    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