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