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