Home | History | Annotate | Download | only in shelf
      1 // Copyright 2013 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 "ash/shelf/shelf_button.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "ash/ash_constants.h"
     10 #include "ash/ash_switches.h"
     11 #include "ash/shelf/shelf_button_host.h"
     12 #include "ash/shelf/shelf_layout_manager.h"
     13 #include "grit/ash_resources.h"
     14 #include "skia/ext/image_operations.h"
     15 #include "ui/base/accessibility/accessible_view_state.h"
     16 #include "ui/base/resource/resource_bundle.h"
     17 #include "ui/compositor/layer.h"
     18 #include "ui/compositor/scoped_layer_animation_settings.h"
     19 #include "ui/events/event_constants.h"
     20 #include "ui/gfx/animation/animation_delegate.h"
     21 #include "ui/gfx/animation/throb_animation.h"
     22 #include "ui/gfx/canvas.h"
     23 #include "ui/gfx/image/image.h"
     24 #include "ui/gfx/image/image_skia_operations.h"
     25 #include "ui/gfx/skbitmap_operations.h"
     26 #include "ui/views/controls/image_view.h"
     27 
     28 namespace {
     29 
     30 // Size of the bar. This is along the opposite axis of the shelf. For example,
     31 // if the shelf is aligned horizontally then this is the height of the bar.
     32 const int kBarSize = 3;
     33 const int kIconSize = 32;
     34 const int kHopSpacing = 2;
     35 const int kIconPad = 8;
     36 const int kAlternateIconPad = 5;
     37 const int kAlternateIconPadVertical = 6;
     38 const int kHopUpMS = 0;
     39 const int kHopDownMS = 200;
     40 const int kAttentionThrobDurationMS = 800;
     41 
     42 bool ShouldHop(int state) {
     43   return state & ash::internal::ShelfButton::STATE_HOVERED ||
     44          state & ash::internal::ShelfButton::STATE_ACTIVE ||
     45          state & ash::internal::ShelfButton::STATE_FOCUSED;
     46 }
     47 
     48 // Simple AnimationDelegate that owns a single ThrobAnimation instance to
     49 // keep all Draw Attention animations in sync.
     50 class ShelfButtonAnimation : public gfx::AnimationDelegate {
     51  public:
     52   class Observer {
     53    public:
     54     virtual void AnimationProgressed() = 0;
     55 
     56    protected:
     57     virtual ~Observer() {}
     58   };
     59 
     60   static ShelfButtonAnimation* GetInstance() {
     61     static ShelfButtonAnimation* s_instance = new ShelfButtonAnimation();
     62     return s_instance;
     63   }
     64 
     65   void AddObserver(Observer* observer) {
     66     observers_.AddObserver(observer);
     67   }
     68 
     69   void RemoveObserver(Observer* observer) {
     70     observers_.RemoveObserver(observer);
     71     if (!observers_.might_have_observers())
     72       animation_.Stop();
     73   }
     74 
     75   int GetAlpha() {
     76     return GetThrobAnimation().CurrentValueBetween(0, 255);
     77   }
     78 
     79   double GetAnimation() {
     80     return GetThrobAnimation().GetCurrentValue();
     81   }
     82 
     83  private:
     84   ShelfButtonAnimation()
     85       : animation_(this) {
     86     animation_.SetThrobDuration(kAttentionThrobDurationMS);
     87     animation_.SetTweenType(gfx::Tween::SMOOTH_IN_OUT);
     88   }
     89 
     90   virtual ~ShelfButtonAnimation() {
     91   }
     92 
     93   gfx::ThrobAnimation& GetThrobAnimation() {
     94     if (!animation_.is_animating()) {
     95       animation_.Reset();
     96       animation_.StartThrobbing(-1 /*throb indefinitely*/);
     97     }
     98     return animation_;
     99   }
    100 
    101   // gfx::AnimationDelegate
    102   virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE {
    103     if (animation != &animation_)
    104       return;
    105     if (!animation_.is_animating())
    106       return;
    107     FOR_EACH_OBSERVER(Observer, observers_, AnimationProgressed());
    108   }
    109 
    110   gfx::ThrobAnimation animation_;
    111   ObserverList<Observer> observers_;
    112 
    113   DISALLOW_COPY_AND_ASSIGN(ShelfButtonAnimation);
    114 };
    115 
    116 }  // namespace
    117 
    118 namespace ash {
    119 namespace internal {
    120 
    121 ////////////////////////////////////////////////////////////////////////////////
    122 // ShelfButton::BarView
    123 
    124 class ShelfButton::BarView : public views::ImageView,
    125                              public ShelfButtonAnimation::Observer {
    126  public:
    127   BarView(ShelfButton* host)
    128       : host_(host),
    129         show_attention_(false) {
    130   }
    131 
    132   virtual ~BarView() {
    133     if (show_attention_)
    134       ShelfButtonAnimation::GetInstance()->RemoveObserver(this);
    135   }
    136 
    137   // View
    138   virtual bool HitTestRect(const gfx::Rect& rect) const OVERRIDE {
    139     // Allow Mouse...() messages to go to the parent view.
    140     return false;
    141   }
    142 
    143   virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
    144     if (show_attention_) {
    145       int alpha = ShelfButtonAnimation::GetInstance()->GetAlpha();
    146       canvas->SaveLayerAlpha(alpha);
    147       views::ImageView::OnPaint(canvas);
    148       canvas->Restore();
    149     } else {
    150       views::ImageView::OnPaint(canvas);
    151     }
    152   }
    153 
    154   // ShelfButtonAnimation::Observer
    155   virtual void AnimationProgressed() OVERRIDE {
    156     UpdateBounds();
    157     SchedulePaint();
    158   }
    159 
    160   void SetBarBoundsRect(const gfx::Rect& bounds) {
    161     base_bounds_ = bounds;
    162     UpdateBounds();
    163   }
    164 
    165   void ShowAttention(bool show) {
    166     if (show_attention_ != show) {
    167       show_attention_ = show;
    168       if (show_attention_)
    169         ShelfButtonAnimation::GetInstance()->AddObserver(this);
    170       else
    171         ShelfButtonAnimation::GetInstance()->RemoveObserver(this);
    172     }
    173     UpdateBounds();
    174   }
    175 
    176  private:
    177   void UpdateBounds() {
    178     gfx::Rect bounds = base_bounds_;
    179     if (show_attention_) {
    180       // Scale from .35 to 1.0 of the total width (which is wider than the
    181       // visible width of the image, so the animation "rests" briefly at full
    182       // visible width.
    183       double animation = ShelfButtonAnimation::GetInstance()->GetAnimation();
    184       double scale = (.35 + .65 * animation);
    185       if (host_->shelf_layout_manager()->GetAlignment() ==
    186           SHELF_ALIGNMENT_BOTTOM) {
    187         bounds.set_width(base_bounds_.width() * scale);
    188         int x_offset = (base_bounds_.width() - bounds.width()) / 2;
    189         bounds.set_x(base_bounds_.x() + x_offset);
    190       } else {
    191         bounds.set_height(base_bounds_.height() * scale);
    192         int y_offset = (base_bounds_.height() - bounds.height()) / 2;
    193         bounds.set_y(base_bounds_.y() + y_offset);
    194       }
    195     }
    196     SetBoundsRect(bounds);
    197   }
    198 
    199   ShelfButton* host_;
    200   bool show_attention_;
    201   gfx::Rect base_bounds_;
    202 
    203   DISALLOW_COPY_AND_ASSIGN(BarView);
    204 };
    205 
    206 ////////////////////////////////////////////////////////////////////////////////
    207 // ShelfButton::IconView
    208 
    209 ShelfButton::IconView::IconView() : icon_size_(kIconSize) {
    210 }
    211 
    212 ShelfButton::IconView::~IconView() {
    213 }
    214 
    215 bool ShelfButton::IconView::HitTestRect(const gfx::Rect& rect) const {
    216   // Return false so that ShelfButton gets all the mouse events.
    217   return false;
    218 }
    219 
    220 ////////////////////////////////////////////////////////////////////////////////
    221 // ShelfButton
    222 
    223 ShelfButton* ShelfButton::Create(views::ButtonListener* listener,
    224                                  ShelfButtonHost* host,
    225                                  ShelfLayoutManager* shelf_layout_manager) {
    226   ShelfButton* button = new ShelfButton(listener, host, shelf_layout_manager);
    227   button->Init();
    228   return button;
    229 }
    230 
    231 ShelfButton::ShelfButton(views::ButtonListener* listener,
    232                          ShelfButtonHost* host,
    233                          ShelfLayoutManager* shelf_layout_manager)
    234     : CustomButton(listener),
    235       host_(host),
    236       icon_view_(NULL),
    237       bar_(new BarView(this)),
    238       state_(STATE_NORMAL),
    239       shelf_layout_manager_(shelf_layout_manager),
    240       destroyed_flag_(NULL) {
    241   SetAccessibilityFocusable(true);
    242 
    243   const gfx::ShadowValue kShadows[] = {
    244     gfx::ShadowValue(gfx::Point(0, 2), 0, SkColorSetARGB(0x1A, 0, 0, 0)),
    245     gfx::ShadowValue(gfx::Point(0, 3), 1, SkColorSetARGB(0x1A, 0, 0, 0)),
    246     gfx::ShadowValue(gfx::Point(0, 0), 1, SkColorSetARGB(0x54, 0, 0, 0)),
    247   };
    248   icon_shadows_.assign(kShadows, kShadows + arraysize(kShadows));
    249 
    250   AddChildView(bar_);
    251 }
    252 
    253 ShelfButton::~ShelfButton() {
    254   if (destroyed_flag_)
    255     *destroyed_flag_ = true;
    256 }
    257 
    258 void ShelfButton::SetShadowedImage(const gfx::ImageSkia& image) {
    259   icon_view_->SetImage(gfx::ImageSkiaOperations::CreateImageWithDropShadow(
    260       image, icon_shadows_));
    261 }
    262 
    263 void ShelfButton::SetImage(const gfx::ImageSkia& image) {
    264   if (image.isNull()) {
    265     // TODO: need an empty image.
    266     icon_view_->SetImage(image);
    267     return;
    268   }
    269 
    270   if (icon_view_->icon_size() == 0) {
    271     SetShadowedImage(image);
    272     return;
    273   }
    274 
    275   // Resize the image maintaining our aspect ratio.
    276   int pref = icon_view_->icon_size();
    277   float aspect_ratio =
    278       static_cast<float>(image.width()) / static_cast<float>(image.height());
    279   int height = pref;
    280   int width = static_cast<int>(aspect_ratio * height);
    281   if (width > pref) {
    282     width = pref;
    283     height = static_cast<int>(width / aspect_ratio);
    284   }
    285 
    286   if (width == image.width() && height == image.height()) {
    287     SetShadowedImage(image);
    288     return;
    289   }
    290 
    291   SetShadowedImage(gfx::ImageSkiaOperations::CreateResizedImage(image,
    292       skia::ImageOperations::RESIZE_BEST, gfx::Size(width, height)));
    293 }
    294 
    295 const gfx::ImageSkia& ShelfButton::GetImage() const {
    296   return icon_view_->GetImage();
    297 }
    298 
    299 void ShelfButton::AddState(State state) {
    300   if (!(state_ & state)) {
    301     if (!ash::switches::UseAlternateShelfLayout() &&
    302         (ShouldHop(state) || !ShouldHop(state_))) {
    303       ui::ScopedLayerAnimationSettings scoped_setter(
    304           icon_view_->layer()->GetAnimator());
    305       scoped_setter.SetTransitionDuration(
    306           base::TimeDelta::FromMilliseconds(kHopUpMS));
    307     }
    308     state_ |= state;
    309     Layout();
    310     if (state & STATE_ATTENTION)
    311       bar_->ShowAttention(true);
    312   }
    313 }
    314 
    315 void ShelfButton::ClearState(State state) {
    316   if (state_ & state) {
    317     if (!ash::switches::UseAlternateShelfLayout() &&
    318         (!ShouldHop(state) || ShouldHop(state_))) {
    319       ui::ScopedLayerAnimationSettings scoped_setter(
    320           icon_view_->layer()->GetAnimator());
    321       scoped_setter.SetTweenType(gfx::Tween::LINEAR);
    322       scoped_setter.SetTransitionDuration(
    323           base::TimeDelta::FromMilliseconds(kHopDownMS));
    324     }
    325     state_ &= ~state;
    326     Layout();
    327     if (state & STATE_ATTENTION)
    328       bar_->ShowAttention(false);
    329   }
    330 }
    331 
    332 gfx::Rect ShelfButton::GetIconBounds() const {
    333   return icon_view_->bounds();
    334 }
    335 
    336 void ShelfButton::ShowContextMenu(const gfx::Point& p,
    337                                   ui::MenuSourceType source_type) {
    338   if (!context_menu_controller())
    339     return;
    340 
    341   bool destroyed = false;
    342   destroyed_flag_ = &destroyed;
    343 
    344   CustomButton::ShowContextMenu(p, source_type);
    345 
    346   if (!destroyed) {
    347     destroyed_flag_ = NULL;
    348     // The menu will not propagate mouse events while its shown. To address,
    349     // the hover state gets cleared once the menu was shown (and this was not
    350     // destroyed).
    351     ClearState(STATE_HOVERED);
    352   }
    353 }
    354 
    355 bool ShelfButton::OnMousePressed(const ui::MouseEvent& event) {
    356   CustomButton::OnMousePressed(event);
    357   host_->PointerPressedOnButton(this, ShelfButtonHost::MOUSE, event);
    358   return true;
    359 }
    360 
    361 void ShelfButton::OnMouseReleased(const ui::MouseEvent& event) {
    362   CustomButton::OnMouseReleased(event);
    363   host_->PointerReleasedOnButton(this, ShelfButtonHost::MOUSE, false);
    364 }
    365 
    366 void ShelfButton::OnMouseCaptureLost() {
    367   ClearState(STATE_HOVERED);
    368   host_->PointerReleasedOnButton(this, ShelfButtonHost::MOUSE, true);
    369   CustomButton::OnMouseCaptureLost();
    370 }
    371 
    372 bool ShelfButton::OnMouseDragged(const ui::MouseEvent& event) {
    373   CustomButton::OnMouseDragged(event);
    374   host_->PointerDraggedOnButton(this, ShelfButtonHost::MOUSE, event);
    375   return true;
    376 }
    377 
    378 void ShelfButton::OnMouseMoved(const ui::MouseEvent& event) {
    379   CustomButton::OnMouseMoved(event);
    380   host_->MouseMovedOverButton(this);
    381 }
    382 
    383 void ShelfButton::OnMouseEntered(const ui::MouseEvent& event) {
    384   AddState(STATE_HOVERED);
    385   CustomButton::OnMouseEntered(event);
    386   host_->MouseEnteredButton(this);
    387 }
    388 
    389 void ShelfButton::OnMouseExited(const ui::MouseEvent& event) {
    390   ClearState(STATE_HOVERED);
    391   CustomButton::OnMouseExited(event);
    392   host_->MouseExitedButton(this);
    393 }
    394 
    395 void ShelfButton::GetAccessibleState(ui::AccessibleViewState* state) {
    396   state->role = ui::AccessibilityTypes::ROLE_PUSHBUTTON;
    397   state->name = host_->GetAccessibleName(this);
    398 }
    399 
    400 void ShelfButton::Layout() {
    401   const gfx::Rect button_bounds(GetContentsBounds());
    402   int icon_pad = kIconPad;
    403   if (ash::switches::UseAlternateShelfLayout()) {
    404       icon_pad =
    405           shelf_layout_manager_->GetAlignment() != SHELF_ALIGNMENT_BOTTOM ?
    406           kAlternateIconPadVertical : kAlternateIconPad;
    407   }
    408   int x_offset = shelf_layout_manager_->PrimaryAxisValue(0, icon_pad);
    409   int y_offset = shelf_layout_manager_->PrimaryAxisValue(icon_pad, 0);
    410 
    411   int icon_width = std::min(kIconSize,
    412       button_bounds.width() - x_offset);
    413   int icon_height = std::min(kIconSize,
    414       button_bounds.height() - y_offset);
    415 
    416   // If on the left or top 'invert' the inset so the constant gap is on
    417   // the interior (towards the center of display) edge of the shelf.
    418   if (SHELF_ALIGNMENT_LEFT == shelf_layout_manager_->GetAlignment())
    419     x_offset = button_bounds.width() - (kIconSize + icon_pad);
    420 
    421   if (SHELF_ALIGNMENT_TOP == shelf_layout_manager_->GetAlignment())
    422     y_offset = button_bounds.height() - (kIconSize + icon_pad);
    423 
    424   if (ShouldHop(state_) && !ash::switches::UseAlternateShelfLayout()) {
    425     x_offset += shelf_layout_manager_->SelectValueForShelfAlignment(
    426         0, kHopSpacing, -kHopSpacing, 0);
    427     y_offset += shelf_layout_manager_->SelectValueForShelfAlignment(
    428         -kHopSpacing, 0, 0, kHopSpacing);
    429   }
    430 
    431   // Center icon with respect to the secondary axis, and ensure
    432   // that the icon doesn't occlude the bar highlight.
    433   if (shelf_layout_manager_->IsHorizontalAlignment()) {
    434     x_offset = std::max(0, button_bounds.width() - icon_width) / 2;
    435     if (y_offset + icon_height + kBarSize > button_bounds.height())
    436       icon_height = button_bounds.height() - (y_offset + kBarSize);
    437   } else {
    438     y_offset = std::max(0, button_bounds.height() - icon_height) / 2;
    439     if (x_offset + icon_width + kBarSize > button_bounds.width())
    440       icon_width = button_bounds.width() - (x_offset + kBarSize);
    441   }
    442 
    443   icon_view_->SetBoundsRect(gfx::Rect(
    444       button_bounds.x() + x_offset,
    445       button_bounds.y() + y_offset,
    446       icon_width,
    447       icon_height));
    448 
    449   // Icon size has been incorrect when running
    450   // PanelLayoutManagerTest.PanelAlignmentSecondDisplay on valgrind bot, see
    451   // http://crbug.com/234854.
    452   DCHECK_LE(icon_width, kIconSize);
    453   DCHECK_LE(icon_height, kIconSize);
    454 
    455   bar_->SetBarBoundsRect(button_bounds);
    456 
    457   UpdateState();
    458 }
    459 
    460 void ShelfButton::ChildPreferredSizeChanged(views::View* child) {
    461   Layout();
    462 }
    463 
    464 void ShelfButton::OnFocus() {
    465   AddState(STATE_FOCUSED);
    466   CustomButton::OnFocus();
    467 }
    468 
    469 void ShelfButton::OnBlur() {
    470   ClearState(STATE_FOCUSED);
    471   CustomButton::OnBlur();
    472 }
    473 
    474 void ShelfButton::OnPaint(gfx::Canvas* canvas) {
    475   CustomButton::OnPaint(canvas);
    476   if (HasFocus()) {
    477     gfx::Rect paint_bounds(GetLocalBounds());
    478     paint_bounds.Inset(1, 1, 1, 1);
    479     canvas->DrawSolidFocusRect(paint_bounds, kFocusBorderColor);
    480   }
    481 }
    482 
    483 void ShelfButton::OnGestureEvent(ui::GestureEvent* event) {
    484   switch (event->type()) {
    485     case ui::ET_GESTURE_TAP_DOWN:
    486       AddState(STATE_HOVERED);
    487       return CustomButton::OnGestureEvent(event);
    488     case ui::ET_GESTURE_END:
    489       ClearState(STATE_HOVERED);
    490       return CustomButton::OnGestureEvent(event);
    491     case ui::ET_GESTURE_SCROLL_BEGIN:
    492       host_->PointerPressedOnButton(this, ShelfButtonHost::TOUCH, *event);
    493       event->SetHandled();
    494       return;
    495     case ui::ET_GESTURE_SCROLL_UPDATE:
    496       host_->PointerDraggedOnButton(this, ShelfButtonHost::TOUCH, *event);
    497       event->SetHandled();
    498       return;
    499     case ui::ET_GESTURE_SCROLL_END:
    500     case ui::ET_SCROLL_FLING_START:
    501       host_->PointerReleasedOnButton(this, ShelfButtonHost::TOUCH, false);
    502       event->SetHandled();
    503       return;
    504     default:
    505       return CustomButton::OnGestureEvent(event);
    506   }
    507 }
    508 
    509 void ShelfButton::Init() {
    510   icon_view_ = CreateIconView();
    511 
    512   // TODO: refactor the layers so each button doesn't require 2.
    513   icon_view_->SetPaintToLayer(true);
    514   icon_view_->SetFillsBoundsOpaquely(false);
    515   icon_view_->SetHorizontalAlignment(views::ImageView::CENTER);
    516   icon_view_->SetVerticalAlignment(views::ImageView::LEADING);
    517 
    518   AddChildView(icon_view_);
    519 }
    520 
    521 ShelfButton::IconView* ShelfButton::CreateIconView() {
    522   return new IconView;
    523 }
    524 
    525 bool ShelfButton::IsShelfHorizontal() const {
    526   return shelf_layout_manager_->IsHorizontalAlignment();
    527 }
    528 
    529 void ShelfButton::UpdateState() {
    530   UpdateBar();
    531 
    532   icon_view_->SetHorizontalAlignment(
    533       shelf_layout_manager_->PrimaryAxisValue(views::ImageView::CENTER,
    534                                               views::ImageView::LEADING));
    535   icon_view_->SetVerticalAlignment(
    536       shelf_layout_manager_->PrimaryAxisValue(views::ImageView::LEADING,
    537                                               views::ImageView::CENTER));
    538   SchedulePaint();
    539 }
    540 
    541 void ShelfButton::UpdateBar() {
    542   if (state_ & STATE_HIDDEN) {
    543     bar_->SetVisible(false);
    544     return;
    545   }
    546 
    547   int bar_id = 0;
    548   if (ash::switches::UseAlternateShelfLayout()) {
    549     if (state_ & STATE_ACTIVE)
    550       bar_id = IDR_AURA_LAUNCHER_UNDERLINE_ACTIVE_ALTERNATE;
    551     else if (state_ & STATE_RUNNING)
    552       bar_id = IDR_AURA_LAUNCHER_UNDERLINE_RUNNING_ALTERNATE;
    553   } else {
    554     if (state_ & (STATE_ACTIVE | STATE_ATTENTION))
    555       bar_id = IDR_AURA_LAUNCHER_UNDERLINE_ACTIVE;
    556     else if (state_ & (STATE_HOVERED | STATE_FOCUSED))
    557       bar_id = IDR_AURA_LAUNCHER_UNDERLINE_HOVER;
    558     else
    559       bar_id = IDR_AURA_LAUNCHER_UNDERLINE_RUNNING;
    560   }
    561 
    562   if (bar_id != 0) {
    563     ResourceBundle& rb = ResourceBundle::GetSharedInstance();
    564     const gfx::ImageSkia* image = rb.GetImageNamed(bar_id).ToImageSkia();
    565     if (shelf_layout_manager_->GetAlignment() == SHELF_ALIGNMENT_BOTTOM) {
    566       bar_->SetImage(*image);
    567     } else {
    568       bar_->SetImage(gfx::ImageSkiaOperations::CreateRotatedImage(*image,
    569           shelf_layout_manager_->SelectValueForShelfAlignment(
    570               SkBitmapOperations::ROTATION_90_CW,
    571               SkBitmapOperations::ROTATION_90_CW,
    572               SkBitmapOperations::ROTATION_270_CW,
    573               SkBitmapOperations::ROTATION_180_CW)));
    574     }
    575     bar_->SetHorizontalAlignment(
    576         shelf_layout_manager_->SelectValueForShelfAlignment(
    577             views::ImageView::CENTER,
    578             views::ImageView::LEADING,
    579             views::ImageView::TRAILING,
    580             views::ImageView::CENTER));
    581     bar_->SetVerticalAlignment(
    582         shelf_layout_manager_->SelectValueForShelfAlignment(
    583             views::ImageView::TRAILING,
    584             views::ImageView::CENTER,
    585             views::ImageView::CENTER,
    586             views::ImageView::LEADING));
    587     bar_->SchedulePaint();
    588   }
    589 
    590   bar_->SetVisible(bar_id != 0 && state_ != STATE_NORMAL);
    591 }
    592 
    593 }  // namespace internal
    594 }  // namespace ash
    595