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