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_app_button.h"
6 
7 #include <algorithm>
8 #include <memory>
9 
10 #include "ash/public/cpp/ash_constants.h"
11 #include "ash/public/cpp/shelf_config.h"
12 #include "ash/public/cpp/shelf_model.h"
13 #include "ash/shelf/shelf.h"
14 #include "ash/shelf/shelf_button_delegate.h"
15 #include "ash/shelf/shelf_view.h"
16 #include "ash/style/default_color_constants.h"
17 #include "ash/style/default_colors.h"
18 #include "ash/wm/tablet_mode/tablet_mode_controller.h"
19 #include "base/bind.h"
20 #include "base/metrics/histogram_macros.h"
21 #include "base/stl_util.h"
22 #include "base/time/time.h"
23 #include "chromeos/constants/chromeos_switches.h"
24 #include "skia/ext/image_operations.h"
25 #include "ui/accessibility/ax_action_data.h"
26 #include "ui/accessibility/ax_node_data.h"
27 #include "ui/base/ui_base_features.h"
28 #include "ui/compositor/layer.h"
29 #include "ui/compositor/scoped_layer_animation_settings.h"
30 #include "ui/gfx/animation/animation_delegate.h"
31 #include "ui/gfx/animation/throb_animation.h"
32 #include "ui/gfx/canvas.h"
33 #include "ui/gfx/color_analysis.h"
34 #include "ui/gfx/geometry/vector2d.h"
35 #include "ui/gfx/image/image_skia_operations.h"
36 #include "ui/gfx/scoped_canvas.h"
37 #include "ui/gfx/skbitmap_operations.h"
38 #include "ui/gfx/transform_util.h"
39 #include "ui/views/animation/ink_drop_impl.h"
40 #include "ui/views/animation/square_ink_drop_ripple.h"
41 #include "ui/views/controls/highlight_path_generator.h"
42 #include "ui/views/controls/image_view.h"
43 #include "ui/views/painter.h"
44 #include "ui/views/style/platform_style.h"
45 
46 namespace {
47 
48 constexpr int kStatusIndicatorRadiusDip = 2;
49 constexpr int kStatusIndicatorMaxSize = 10;
50 constexpr int kStatusIndicatorActiveSize = 8;
51 constexpr int kStatusIndicatorRunningSize = 4;
52 constexpr int kStatusIndicatorThickness = 2;
53 
54 constexpr int kNotificationIndicatorRadiusDip = 6;
55 constexpr int kNotificationIndicatorPadding = 1;
56 
57 constexpr SkColor kDefaultIndicatorColor = SK_ColorWHITE;
58 
59 // The time threshold before an item can be dragged.
60 constexpr int kDragTimeThresholdMs = 300;
61 
62 // The time threshold before the ink drop should activate on a long press.
63 constexpr int kInkDropRippleActivationTimeMs = 650;
64 
65 // The drag and drop app icon should get scaled by this factor.
66 constexpr float kAppIconScale = 1.2f;
67 
68 // The drag and drop app icon scaling up or down animation transition duration.
69 constexpr int kDragDropAppIconScaleTransitionMs = 200;
70 
71 // Uses the icon image to calculate the light vibrant color to be used for
72 // the notification indicator.
CalculateNotificationColor(gfx::ImageSkia image)73 base::Optional<SkColor> CalculateNotificationColor(gfx::ImageSkia image) {
74   const SkBitmap* source = image.bitmap();
75   if (!source || source->empty() || source->isNull())
76     return base::nullopt;
77 
78   std::vector<color_utils::ColorProfile> color_profiles;
79   color_profiles.push_back(color_utils::ColorProfile(
80       color_utils::LumaRange::LIGHT, color_utils::SaturationRange::VIBRANT));
81 
82   std::vector<color_utils::Swatch> best_swatches =
83       color_utils::CalculateProminentColorsOfBitmap(
84           *source, color_profiles, nullptr /* bitmap region */,
85           color_utils::ColorSwatchFilter());
86 
87   // If the best swatch color is transparent, then
88   // CalculateProminentColorsOfBitmap() failed to find a suitable color.
89   if (best_swatches.empty() || best_swatches[0].color == SK_ColorTRANSPARENT)
90     return base::nullopt;
91 
92   return best_swatches[0].color;
93 }
94 
95 // Simple AnimationDelegate that owns a single ThrobAnimation instance to
96 // keep all Draw Attention animations in sync.
97 class ShelfAppButtonAnimation : public gfx::AnimationDelegate {
98  public:
99   class Observer {
100    public:
101     virtual void AnimationProgressed() = 0;
102 
103    protected:
104     virtual ~Observer() = default;
105   };
106 
GetInstance()107   static ShelfAppButtonAnimation* GetInstance() {
108     static ShelfAppButtonAnimation* s_instance = new ShelfAppButtonAnimation();
109     return s_instance;
110   }
111 
AddObserver(Observer * observer)112   void AddObserver(Observer* observer) { observers_.AddObserver(observer); }
113 
RemoveObserver(Observer * observer)114   void RemoveObserver(Observer* observer) {
115     observers_.RemoveObserver(observer);
116     if (!observers_.might_have_observers())
117       animation_.Stop();
118   }
119 
HasObserver(Observer * observer) const120   bool HasObserver(Observer* observer) const {
121     return observers_.HasObserver(observer);
122   }
123 
GetAlpha()124   SkAlpha GetAlpha() {
125     return GetThrobAnimation().CurrentValueBetween(SK_AlphaTRANSPARENT,
126                                                    SK_AlphaOPAQUE);
127   }
128 
GetAnimation()129   double GetAnimation() { return GetThrobAnimation().GetCurrentValue(); }
130 
131  private:
ShelfAppButtonAnimation()132   ShelfAppButtonAnimation() : animation_(this) {
133     animation_.SetThrobDuration(base::TimeDelta::FromMilliseconds(800));
134     animation_.SetTweenType(gfx::Tween::SMOOTH_IN_OUT);
135   }
136 
137   ~ShelfAppButtonAnimation() override = default;
138 
GetThrobAnimation()139   gfx::ThrobAnimation& GetThrobAnimation() {
140     if (!animation_.is_animating()) {
141       animation_.Reset();
142       animation_.StartThrobbing(-1 /*throb indefinitely*/);
143     }
144     return animation_;
145   }
146 
147   // gfx::AnimationDelegate
AnimationProgressed(const gfx::Animation * animation)148   void AnimationProgressed(const gfx::Animation* animation) override {
149     if (animation != &animation_)
150       return;
151     if (!animation_.is_animating())
152       return;
153     for (auto& observer : observers_)
154       observer.AnimationProgressed();
155   }
156 
157   gfx::ThrobAnimation animation_;
158   base::ObserverList<Observer>::Unchecked observers_;
159 
160   DISALLOW_COPY_AND_ASSIGN(ShelfAppButtonAnimation);
161 };
162 
163 }  // namespace
164 
165 namespace ash {
166 
167 ////////////////////////////////////////////////////////////////////////////////
168 // ShelfAppButton::AppNotificationIndicatorView
169 
170 // The indicator which is activated when the app corresponding with this
171 // ShelfAppButton receives a notification.
172 class ShelfAppButton::AppNotificationIndicatorView : public views::View {
173  public:
AppNotificationIndicatorView(SkColor indicator_color)174   explicit AppNotificationIndicatorView(SkColor indicator_color)
175       : indicator_color_(indicator_color) {}
176 
~AppNotificationIndicatorView()177   ~AppNotificationIndicatorView() override {}
178 
OnPaint(gfx::Canvas * canvas)179   void OnPaint(gfx::Canvas* canvas) override {
180     gfx::ScopedCanvas scoped(canvas);
181 
182     canvas->SaveLayerAlpha(SK_AlphaOPAQUE);
183 
184     DCHECK_EQ(width(), height());
185     DCHECK_EQ(kNotificationIndicatorRadiusDip, width() / 2);
186     const float dsf = canvas->UndoDeviceScaleFactor();
187     const int kStrokeWidthPx = 1;
188     gfx::PointF center = gfx::RectF(GetLocalBounds()).CenterPoint();
189     center.Scale(dsf);
190 
191     // Fill the center.
192     cc::PaintFlags flags;
193     flags.setColor(indicator_color_);
194     flags.setAntiAlias(true);
195     canvas->DrawCircle(
196         center, dsf * kNotificationIndicatorRadiusDip - kStrokeWidthPx, flags);
197 
198     // Stroke the border.
199     flags.setColor(SkColorSetA(SK_ColorBLACK, 0x4D));
200     flags.setStyle(cc::PaintFlags::kStroke_Style);
201     canvas->DrawCircle(
202         center, dsf * kNotificationIndicatorRadiusDip - kStrokeWidthPx / 2.0f,
203         flags);
204   }
205 
SetColor(SkColor new_color)206   void SetColor(SkColor new_color) {
207     indicator_color_ = new_color;
208     SchedulePaint();
209   }
210 
GetColorForTest()211   SkColor GetColorForTest() { return indicator_color_; }
212 
213  private:
214   SkColor indicator_color_;
215 
216   DISALLOW_COPY_AND_ASSIGN(AppNotificationIndicatorView);
217 };
218 
219 ////////////////////////////////////////////////////////////////////////////////
220 // ShelfAppButton::AppStatusIndicatorView
221 
222 class ShelfAppButton::AppStatusIndicatorView
223     : public views::View,
224       public ShelfAppButtonAnimation::Observer {
225  public:
AppStatusIndicatorView()226   AppStatusIndicatorView() : show_attention_(false), active_(false) {
227     // Make sure the events reach the parent view for handling.
228     SetCanProcessEventsWithinSubtree(false);
229   }
230 
~AppStatusIndicatorView()231   ~AppStatusIndicatorView() override {
232     ShelfAppButtonAnimation::GetInstance()->RemoveObserver(this);
233   }
234 
235   // views::View:
OnPaint(gfx::Canvas * canvas)236   void OnPaint(gfx::Canvas* canvas) override {
237     gfx::ScopedCanvas scoped(canvas);
238     if (show_attention_) {
239       const SkAlpha alpha =
240           ShelfAppButtonAnimation::GetInstance()->HasObserver(this)
241               ? ShelfAppButtonAnimation::GetInstance()->GetAlpha()
242               : SK_AlphaOPAQUE;
243       canvas->SaveLayerAlpha(alpha);
244     }
245 
246     const float dsf = canvas->UndoDeviceScaleFactor();
247     gfx::PointF center = gfx::RectF(GetLocalBounds()).CenterPoint();
248     cc::PaintFlags flags;
249     // Active and running indicators look a little different in the new UI.
250     flags.setColor(DeprecatedGetAppStateIndicatorColor(
251         active_, kIndicatorColorActive, kInicatorColorRunning));
252     flags.setAntiAlias(true);
253     flags.setStrokeCap(cc::PaintFlags::Cap::kRound_Cap);
254     flags.setStrokeJoin(cc::PaintFlags::Join::kRound_Join);
255     flags.setStrokeWidth(kStatusIndicatorThickness);
256     flags.setStyle(cc::PaintFlags::kStroke_Style);
257     float stroke_length =
258         active_ ? kStatusIndicatorActiveSize : kStatusIndicatorRunningSize;
259     gfx::PointF start;
260     gfx::PointF end;
261     if (horizontal_shelf_) {
262       start = gfx::PointF(center.x() - stroke_length / 2, center.y());
263       end = start;
264       end.Offset(stroke_length, 0);
265     } else {
266       start = gfx::PointF(center.x(), center.y() - stroke_length / 2);
267       end = start;
268       end.Offset(0, stroke_length);
269     }
270     SkPath path;
271     path.moveTo(start.x() * dsf, start.y() * dsf);
272     path.lineTo(end.x() * dsf, end.y() * dsf);
273     canvas->DrawPath(path, flags);
274   }
275 
276   // ShelfAppButtonAnimation::Observer
AnimationProgressed()277   void AnimationProgressed() override {
278     UpdateAnimating();
279     SchedulePaint();
280   }
281 
ShowAttention(bool show)282   void ShowAttention(bool show) {
283     if (show_attention_ == show)
284       return;
285 
286     show_attention_ = show;
287     if (show_attention_) {
288       animation_end_time_ =
289           base::TimeTicks::Now() + base::TimeDelta::FromSeconds(10);
290       ShelfAppButtonAnimation::GetInstance()->AddObserver(this);
291     } else {
292       ShelfAppButtonAnimation::GetInstance()->RemoveObserver(this);
293     }
294   }
295 
ShowActiveStatus(bool active)296   void ShowActiveStatus(bool active) {
297     if (active_ == active)
298       return;
299     active_ = active;
300     SchedulePaint();
301   }
302 
SetHorizontalShelf(bool horizontal_shelf)303   void SetHorizontalShelf(bool horizontal_shelf) {
304     if (horizontal_shelf_ == horizontal_shelf)
305       return;
306     horizontal_shelf_ = horizontal_shelf;
307     SchedulePaint();
308   }
309 
310  private:
UpdateAnimating()311   void UpdateAnimating() {
312     if (base::TimeTicks::Now() > animation_end_time_)
313       ShelfAppButtonAnimation::GetInstance()->RemoveObserver(this);
314   }
315 
316   bool show_attention_ = false;
317   bool active_ = false;
318   bool horizontal_shelf_ = true;
319   base::TimeTicks animation_end_time_;  // For attention throbbing underline.
320 
321   DISALLOW_COPY_AND_ASSIGN(AppStatusIndicatorView);
322 };
323 
324 ////////////////////////////////////////////////////////////////////////////////
325 // ShelfAppButton
326 
327 // static
328 const char ShelfAppButton::kViewClassName[] = "ash/ShelfAppButton";
329 
330 // static
ShouldHandleEventFromContextMenu(const ui::GestureEvent * event)331 bool ShelfAppButton::ShouldHandleEventFromContextMenu(
332     const ui::GestureEvent* event) {
333   switch (event->type()) {
334     case ui::ET_GESTURE_END:
335     case ui::ET_GESTURE_TAP_CANCEL:
336     case ui::ET_GESTURE_SCROLL_BEGIN:
337     case ui::ET_GESTURE_SCROLL_UPDATE:
338     case ui::ET_GESTURE_SCROLL_END:
339     case ui::ET_SCROLL_FLING_START:
340       return true;
341     default:
342       return false;
343   }
344 }
345 
ShelfAppButton(ShelfView * shelf_view,ShelfButtonDelegate * shelf_button_delegate)346 ShelfAppButton::ShelfAppButton(ShelfView* shelf_view,
347                                ShelfButtonDelegate* shelf_button_delegate)
348     : ShelfButton(shelf_view->shelf(), shelf_button_delegate),
349       icon_view_(new views::ImageView()),
350       shelf_view_(shelf_view),
351       indicator_(new AppStatusIndicatorView()),
352       notification_indicator_(nullptr),
353       state_(STATE_NORMAL),
354       destroyed_flag_(nullptr),
355       is_notification_indicator_enabled_(
356           features::IsNotificationIndicatorEnabled()) {
357   const gfx::ShadowValue kShadows[] = {
358       gfx::ShadowValue(gfx::Vector2d(0, 2), 0, SkColorSetARGB(0x1A, 0, 0, 0)),
359       gfx::ShadowValue(gfx::Vector2d(0, 3), 1, SkColorSetARGB(0x1A, 0, 0, 0)),
360       gfx::ShadowValue(gfx::Vector2d(0, 0), 1, SkColorSetARGB(0x54, 0, 0, 0)),
361   };
362   icon_shadows_.assign(kShadows, kShadows + base::size(kShadows));
363 
364   // TODO: refactor the layers so each button doesn't require 3.
365   // |icon_view_| needs its own layer so it can be scaled up independently of
366   // the ink drop ripple.
367   icon_view_->SetPaintToLayer();
368   icon_view_->layer()->SetFillsBoundsOpaquely(false);
369   icon_view_->SetHorizontalAlignment(views::ImageView::Alignment::kCenter);
370   icon_view_->SetVerticalAlignment(views::ImageView::Alignment::kLeading);
371   // Do not make this interactive, so that events are sent to ShelfView.
372   icon_view_->SetCanProcessEventsWithinSubtree(false);
373 
374   indicator_->SetPaintToLayer();
375   indicator_->layer()->SetFillsBoundsOpaquely(false);
376 
377   AddChildView(indicator_);
378   AddChildView(icon_view_);
379   if (is_notification_indicator_enabled_) {
380     notification_indicator_ =
381         new AppNotificationIndicatorView(kDefaultIndicatorColor);
382     notification_indicator_->SetPaintToLayer();
383     notification_indicator_->layer()->SetFillsBoundsOpaquely(false);
384     notification_indicator_->SetVisible(false);
385     AddChildView(notification_indicator_);
386   }
387   GetInkDrop()->AddObserver(this);
388 
389   // Do not set a clip, allow the ink drop to burst out.
390   views::InstallEmptyHighlightPathGenerator(this);
391   SetFocusBehavior(FocusBehavior::ALWAYS);
392   SetInstallFocusRingOnFocus(true);
393   focus_ring()->SetColor(ShelfConfig::Get()->shelf_focus_border_color());
394   // The focus ring should have an inset of half the focus border thickness, so
395   // the parent view won't clip it.
396   focus_ring()->SetPathGenerator(
397       std::make_unique<views::RoundRectHighlightPathGenerator>(
398           gfx::Insets(views::PlatformStyle::kFocusHaloThickness / 2, 0), 0));
399 }
400 
~ShelfAppButton()401 ShelfAppButton::~ShelfAppButton() {
402   GetInkDrop()->RemoveObserver(this);
403   if (destroyed_flag_)
404     *destroyed_flag_ = true;
405 }
406 
SetShadowedImage(const gfx::ImageSkia & image)407 void ShelfAppButton::SetShadowedImage(const gfx::ImageSkia& image) {
408   icon_view_->SetImage(gfx::ImageSkiaOperations::CreateImageWithDropShadow(
409       image, icon_shadows_));
410 }
411 
SetImage(const gfx::ImageSkia & image)412 void ShelfAppButton::SetImage(const gfx::ImageSkia& image) {
413   if (image.isNull()) {
414     // TODO: need an empty image.
415     icon_view_->SetImage(image);
416     icon_image_ = gfx::ImageSkia();
417     return;
418   }
419   icon_image_ = image;
420 
421   if (is_notification_indicator_enabled_) {
422     base::Optional<SkColor> notification_color =
423         CalculateNotificationColor(icon_image_);
424     notification_indicator_->SetColor(
425         notification_color.value_or(kDefaultIndicatorColor));
426   }
427 
428   const int icon_size = shelf_view_->GetButtonIconSize() * icon_scale_;
429 
430   // Resize the image maintaining our aspect ratio.
431   float aspect_ratio = static_cast<float>(icon_image_.width()) /
432                        static_cast<float>(icon_image_.height());
433   int height = icon_size;
434   int width = static_cast<int>(aspect_ratio * height);
435   if (width > icon_size) {
436     width = icon_size;
437     height = static_cast<int>(width / aspect_ratio);
438   }
439 
440   const gfx::Size preferred_size(width, height);
441 
442   if (image.size() == preferred_size) {
443     SetShadowedImage(image);
444     return;
445   }
446 
447   SetShadowedImage(gfx::ImageSkiaOperations::CreateResizedImage(
448       image, skia::ImageOperations::RESIZE_BEST, preferred_size));
449 }
450 
GetImage() const451 const gfx::ImageSkia& ShelfAppButton::GetImage() const {
452   return icon_view_->GetImage();
453 }
454 
AddState(State state)455 void ShelfAppButton::AddState(State state) {
456   if (!(state_ & state)) {
457     state_ |= state;
458     InvalidateLayout();
459     if (state & STATE_ATTENTION)
460       indicator_->ShowAttention(true);
461 
462     if (state & STATE_ACTIVE)
463       indicator_->ShowActiveStatus(true);
464 
465     if (is_notification_indicator_enabled_ && (state & STATE_NOTIFICATION))
466       notification_indicator_->SetVisible(true);
467 
468     if (state & STATE_DRAGGING)
469       ScaleAppIcon(true);
470   }
471 }
472 
ClearState(State state)473 void ShelfAppButton::ClearState(State state) {
474   if (state_ & state) {
475     state_ &= ~state;
476     Layout();
477     if (state & STATE_ATTENTION)
478       indicator_->ShowAttention(false);
479     if (state & STATE_ACTIVE)
480       indicator_->ShowActiveStatus(false);
481 
482     if (is_notification_indicator_enabled_ && (state & STATE_NOTIFICATION))
483       notification_indicator_->SetVisible(false);
484 
485     if (state & STATE_DRAGGING)
486       ScaleAppIcon(false);
487   }
488 }
489 
ClearDragStateOnGestureEnd()490 void ShelfAppButton::ClearDragStateOnGestureEnd() {
491   drag_timer_.Stop();
492   ClearState(STATE_HOVERED);
493   ClearState(STATE_DRAGGING);
494 }
495 
GetIconBounds() const496 gfx::Rect ShelfAppButton::GetIconBounds() const {
497   return icon_view_->bounds();
498 }
499 
GetIconBoundsInScreen() const500 gfx::Rect ShelfAppButton::GetIconBoundsInScreen() const {
501   return icon_view_->GetBoundsInScreen();
502 }
503 
GetInkDropForTesting()504 views::InkDrop* ShelfAppButton::GetInkDropForTesting() {
505   return GetInkDrop();
506 }
507 
OnDragStarted(const ui::LocatedEvent * event)508 void ShelfAppButton::OnDragStarted(const ui::LocatedEvent* event) {
509   AnimateInkDrop(views::InkDropState::HIDDEN, event);
510 }
511 
OnMenuClosed()512 void ShelfAppButton::OnMenuClosed() {
513   DCHECK_EQ(views::InkDropState::ACTIVATED,
514             GetInkDrop()->GetTargetInkDropState());
515   GetInkDrop()->AnimateToState(views::InkDropState::DEACTIVATED);
516 }
517 
ShowContextMenu(const gfx::Point & p,ui::MenuSourceType source_type)518 void ShelfAppButton::ShowContextMenu(const gfx::Point& p,
519                                      ui::MenuSourceType source_type) {
520   if (!context_menu_controller())
521     return;
522 
523   bool destroyed = false;
524   destroyed_flag_ = &destroyed;
525 
526   if (source_type == ui::MenuSourceType::MENU_SOURCE_MOUSE ||
527       source_type == ui::MenuSourceType::MENU_SOURCE_KEYBOARD) {
528     GetInkDrop()->AnimateToState(views::InkDropState::ACTIVATED);
529   }
530 
531   ShelfButton::ShowContextMenu(p, source_type);
532 
533   if (!destroyed) {
534     destroyed_flag_ = nullptr;
535     // The menu will not propagate mouse events while it's shown. To address,
536     // the hover state gets cleared once the menu was shown (and this was not
537     // destroyed). In case context menu is shown target view does not receive
538     // OnMouseReleased events and we need to cancel capture manually.
539     if (shelf_view_->IsDraggedView(this))
540       OnMouseCaptureLost();
541     else
542       ClearState(STATE_HOVERED);
543   }
544 }
545 
GetAccessibleNodeData(ui::AXNodeData * node_data)546 void ShelfAppButton::GetAccessibleNodeData(ui::AXNodeData* node_data) {
547   ShelfButton::GetAccessibleNodeData(node_data);
548   const base::string16 title = shelf_view_->GetTitleForView(this);
549   node_data->SetName(title.empty() ? GetAccessibleName() : title);
550 }
551 
ShouldEnterPushedState(const ui::Event & event)552 bool ShelfAppButton::ShouldEnterPushedState(const ui::Event& event) {
553   if (!shelf_view_->ShouldEventActivateButton(this, event))
554     return false;
555 
556   return Button::ShouldEnterPushedState(event);
557 }
558 
ReflectItemStatus(const ShelfItem & item)559 void ShelfAppButton::ReflectItemStatus(const ShelfItem& item) {
560   if (features::IsNotificationIndicatorEnabled()) {
561     if (item.has_notification)
562       AddState(ShelfAppButton::STATE_NOTIFICATION);
563     else
564       ClearState(ShelfAppButton::STATE_NOTIFICATION);
565   }
566 
567   const ShelfID active_id = shelf_view_->model()->active_shelf_id();
568   if (!active_id.IsNull() && item.id == active_id) {
569     // The active status trumps all other statuses.
570     AddState(ShelfAppButton::STATE_ACTIVE);
571     ClearState(ShelfAppButton::STATE_RUNNING);
572     ClearState(ShelfAppButton::STATE_ATTENTION);
573     return;
574   }
575 
576   ClearState(ShelfAppButton::STATE_ACTIVE);
577 
578   switch (item.status) {
579     case STATUS_CLOSED:
580       ClearState(ShelfAppButton::STATE_RUNNING);
581       ClearState(ShelfAppButton::STATE_ATTENTION);
582       break;
583     case STATUS_RUNNING:
584       AddState(ShelfAppButton::STATE_RUNNING);
585       ClearState(ShelfAppButton::STATE_ATTENTION);
586       break;
587     case STATUS_ATTENTION:
588       ClearState(ShelfAppButton::STATE_RUNNING);
589       AddState(ShelfAppButton::STATE_ATTENTION);
590       break;
591   }
592 }
593 
IsIconSizeCurrent()594 bool ShelfAppButton::IsIconSizeCurrent() {
595   gfx::Insets insets_shadows = gfx::ShadowValue::GetMargin(icon_shadows_);
596   int icon_width =
597       GetIconBounds().width() + insets_shadows.left() + insets_shadows.right();
598 
599   return icon_width == shelf_view_->GetButtonIconSize();
600 }
601 
FireDragTimerForTest()602 bool ShelfAppButton::FireDragTimerForTest() {
603   if (!drag_timer_.IsRunning())
604     return false;
605   drag_timer_.FireNow();
606   return true;
607 }
608 
FireRippleActivationTimerForTest()609 void ShelfAppButton::FireRippleActivationTimerForTest() {
610   ripple_activation_timer_.FireNow();
611 }
612 
CalculateSmallRippleArea() const613 gfx::Rect ShelfAppButton::CalculateSmallRippleArea() const {
614   int ink_drop_small_size = shelf_view_->GetButtonSize();
615   gfx::Point center_point = GetLocalBounds().CenterPoint();
616   const int padding = ShelfConfig::Get()->GetAppIconEndPadding();
617 
618   // Add padding to the ink drop for the left-most and right-most app buttons in
619   // the shelf when there is a non-zero padding between the app icon and the
620   // end of scrollable shelf.
621   if (TabletModeController::Get()->InTabletMode() && padding > 0) {
622     const int current_index = shelf_view_->view_model()->GetIndexOfView(this);
623     int left_padding =
624         (shelf_view_->visible_views_indices().front() == current_index)
625             ? padding
626             : 0;
627     int right_padding =
628         (shelf_view_->visible_views_indices().back() == current_index) ? padding
629                                                                        : 0;
630 
631     if (base::i18n::IsRTL())
632       std::swap(left_padding, right_padding);
633 
634     ink_drop_small_size += left_padding + right_padding;
635 
636     const int x_offset = (-left_padding / 2) + (right_padding / 2);
637     center_point.Offset(x_offset, 0);
638   }
639 
640   gfx::Rect small_ripple_area(
641       gfx::Size(ink_drop_small_size, ink_drop_small_size));
642   small_ripple_area.Offset(center_point.x() - ink_drop_small_size / 2,
643                            center_point.y() - ink_drop_small_size / 2);
644   return small_ripple_area;
645 }
646 
GetClassName() const647 const char* ShelfAppButton::GetClassName() const {
648   return kViewClassName;
649 }
650 
OnMousePressed(const ui::MouseEvent & event)651 bool ShelfAppButton::OnMousePressed(const ui::MouseEvent& event) {
652   // TODO: This call should probably live somewhere else (such as inside
653   // |ShelfView.PointerPressedOnButton|.
654   // No need to scale up the app for mouse right click since the app can't be
655   // dragged through right button.
656   if (!(event.flags() & ui::EF_LEFT_MOUSE_BUTTON)) {
657     Button::OnMousePressed(event);
658     return true;
659   }
660 
661   ShelfButton::OnMousePressed(event);
662   shelf_view_->PointerPressedOnButton(this, ShelfView::MOUSE, event);
663 
664   if (shelf_view_->IsDraggedView(this)) {
665     drag_timer_.Start(FROM_HERE,
666                       base::TimeDelta::FromMilliseconds(kDragTimeThresholdMs),
667                       base::BindOnce(&ShelfAppButton::OnTouchDragTimer,
668                                      base::Unretained(this)));
669   }
670   return true;
671 }
672 
OnMouseReleased(const ui::MouseEvent & event)673 void ShelfAppButton::OnMouseReleased(const ui::MouseEvent& event) {
674   drag_timer_.Stop();
675   ClearState(STATE_DRAGGING);
676   ShelfButton::OnMouseReleased(event);
677   // PointerReleasedOnButton deletes the ShelfAppButton when user drags a pinned
678   // running app from shelf.
679   shelf_view_->PointerReleasedOnButton(this, ShelfView::MOUSE, false);
680   // WARNING: we may have been deleted.
681 }
682 
OnMouseCaptureLost()683 void ShelfAppButton::OnMouseCaptureLost() {
684   ClearState(STATE_HOVERED);
685   shelf_view_->PointerReleasedOnButton(this, ShelfView::MOUSE, true);
686   ShelfButton::OnMouseCaptureLost();
687 }
688 
OnMouseDragged(const ui::MouseEvent & event)689 bool ShelfAppButton::OnMouseDragged(const ui::MouseEvent& event) {
690   ShelfButton::OnMouseDragged(event);
691   shelf_view_->PointerDraggedOnButton(this, ShelfView::MOUSE, event);
692   return true;
693 }
694 
GetIconViewBounds(float icon_scale)695 gfx::Rect ShelfAppButton::GetIconViewBounds(float icon_scale) {
696   const float icon_size = shelf_view_->GetButtonIconSize() * icon_scale;
697   const float icon_padding = (shelf_view_->GetButtonSize() - icon_size) / 2;
698 
699   const gfx::Rect button_bounds(GetContentsBounds());
700   const Shelf* shelf = shelf_view_->shelf();
701   const bool is_horizontal_shelf = shelf->IsHorizontalAlignment();
702   float x_offset = is_horizontal_shelf ? 0 : icon_padding;
703   float y_offset = is_horizontal_shelf ? icon_padding : 0;
704 
705   const float icon_width =
706       std::min(icon_size, button_bounds.width() - x_offset);
707   const float icon_height =
708       std::min(icon_size, button_bounds.height() - y_offset);
709 
710   // If on the left or top 'invert' the inset so the constant gap is on
711   // the interior (towards the center of display) edge of the shelf.
712   if (ShelfAlignment::kLeft == shelf->alignment())
713     x_offset = button_bounds.width() - (icon_size + icon_padding);
714 
715   // Expand bounds to include shadows.
716   gfx::Insets insets_shadows = gfx::ShadowValue::GetMargin(icon_shadows_);
717   // insets_shadows = insets_shadows.Scale(icon_scale);
718   // Center icon with respect to the secondary axis.
719   if (is_horizontal_shelf)
720     x_offset = std::max(0.0f, button_bounds.width() - icon_width + 1) / 2;
721   else
722     y_offset = std::max(0.0f, button_bounds.height() - icon_height) / 2;
723   gfx::RectF icon_view_bounds =
724       gfx::RectF(button_bounds.x() + x_offset, button_bounds.y() + y_offset,
725                  icon_width, icon_height);
726 
727   icon_view_bounds.Inset(insets_shadows);
728   // Icon size has been incorrect when running
729   // PanelLayoutManagerTest.PanelAlignmentSecondDisplay on valgrind bot, see
730   // http://crbug.com/234854.
731   DCHECK_LE(icon_width, icon_size);
732   DCHECK_LE(icon_height, icon_size);
733   return gfx::ToRoundedRect(icon_view_bounds);
734 }
735 
Layout()736 void ShelfAppButton::Layout() {
737   Shelf* shelf = shelf_view_->shelf();
738   gfx::Rect icon_view_bounds = GetIconViewBounds(icon_scale_);
739   const gfx::Rect button_bounds(GetContentsBounds());
740   const int status_indicator_offet_from_shelf_edge =
741       ShelfConfig::Get()->status_indicator_offset_from_shelf_edge();
742 
743   icon_view_->SetBoundsRect(icon_view_bounds);
744 
745   // The indicators should be aligned with the icon, not the icon + shadow.
746   gfx::Point indicator_midpoint = icon_view_bounds.CenterPoint();
747   if (is_notification_indicator_enabled_) {
748     notification_indicator_->SetBoundsRect(gfx::Rect(
749         icon_view_bounds.right() - 2 * kNotificationIndicatorRadiusDip -
750             kNotificationIndicatorPadding,
751         icon_view_bounds.y() + kNotificationIndicatorPadding,
752         kNotificationIndicatorRadiusDip * 2,
753         kNotificationIndicatorRadiusDip * 2));
754   }
755 
756   switch (shelf->alignment()) {
757     case ShelfAlignment::kBottom:
758     case ShelfAlignment::kBottomLocked:
759       indicator_midpoint.set_y(button_bounds.bottom() -
760                                kStatusIndicatorRadiusDip -
761                                status_indicator_offet_from_shelf_edge);
762       break;
763     case ShelfAlignment::kLeft:
764       indicator_midpoint.set_x(button_bounds.x() + kStatusIndicatorRadiusDip +
765                                status_indicator_offet_from_shelf_edge);
766       break;
767     case ShelfAlignment::kRight:
768       indicator_midpoint.set_x(button_bounds.right() -
769                                kStatusIndicatorRadiusDip -
770                                status_indicator_offet_from_shelf_edge);
771       break;
772   }
773 
774   gfx::Rect indicator_bounds(indicator_midpoint, gfx::Size());
775   indicator_bounds.Inset(gfx::Insets(-kStatusIndicatorMaxSize));
776   indicator_->SetBoundsRect(indicator_bounds);
777 
778   UpdateState();
779   focus_ring()->Layout();
780 }
781 
ChildPreferredSizeChanged(views::View * child)782 void ShelfAppButton::ChildPreferredSizeChanged(views::View* child) {
783   Layout();
784 }
785 
OnGestureEvent(ui::GestureEvent * event)786 void ShelfAppButton::OnGestureEvent(ui::GestureEvent* event) {
787   switch (event->type()) {
788     case ui::ET_GESTURE_TAP_DOWN:
789       if (shelf_view_->shelf()->IsVisible()) {
790         AddState(STATE_HOVERED);
791         drag_timer_.Start(
792             FROM_HERE, base::TimeDelta::FromMilliseconds(kDragTimeThresholdMs),
793             base::BindOnce(&ShelfAppButton::OnTouchDragTimer,
794                            base::Unretained(this)));
795         ripple_activation_timer_.Start(
796             FROM_HERE,
797             base::TimeDelta::FromMilliseconds(kInkDropRippleActivationTimeMs),
798             base::BindOnce(&ShelfAppButton::OnRippleTimer,
799                            base::Unretained(this)));
800         GetInkDrop()->AnimateToState(views::InkDropState::ACTION_PENDING);
801         event->SetHandled();
802       }
803       break;
804     case ui::ET_GESTURE_TAP:
805       FALLTHROUGH;  // Ensure tapped items are not enlarged for drag.
806     case ui::ET_GESTURE_END:
807       // If the button is being dragged, or there is an active context menu,
808       // for this ShelfAppButton, don't deactivate the ink drop.
809       if (!(state_ & STATE_DRAGGING) &&
810           !shelf_view_->IsShowingMenuForView(this) &&
811           (GetInkDrop()->GetTargetInkDropState() ==
812            views::InkDropState::ACTIVATED)) {
813         GetInkDrop()->AnimateToState(views::InkDropState::DEACTIVATED);
814       }
815       ClearDragStateOnGestureEnd();
816       break;
817     case ui::ET_GESTURE_SCROLL_BEGIN:
818       if (state_ & STATE_DRAGGING) {
819         shelf_view_->PointerPressedOnButton(this, ShelfView::TOUCH, *event);
820         event->SetHandled();
821       } else {
822         // The drag went to the bezel and is about to be passed to
823         // ShelfLayoutManager.
824         drag_timer_.Stop();
825         GetInkDrop()->AnimateToState(views::InkDropState::HIDDEN);
826       }
827       break;
828     case ui::ET_GESTURE_SCROLL_UPDATE:
829       if ((state_ & STATE_DRAGGING) && shelf_view_->IsDraggedView(this)) {
830         shelf_view_->PointerDraggedOnButton(this, ShelfView::TOUCH, *event);
831         event->SetHandled();
832       }
833       break;
834     case ui::ET_GESTURE_SCROLL_END:
835     case ui::ET_SCROLL_FLING_START:
836       if (state_ & STATE_DRAGGING) {
837         ClearState(STATE_DRAGGING);
838         shelf_view_->PointerReleasedOnButton(this, ShelfView::TOUCH, false);
839         event->SetHandled();
840       }
841       break;
842     case ui::ET_GESTURE_LONG_TAP:
843       GetInkDrop()->AnimateToState(views::InkDropState::ACTIVATED);
844       // Handle LONG_TAP to avoid opening the context menu twice.
845       event->SetHandled();
846       break;
847     case ui::ET_GESTURE_TWO_FINGER_TAP:
848       GetInkDrop()->AnimateToState(views::InkDropState::ACTIVATED);
849       break;
850     default:
851       break;
852   }
853 
854   if (!event->handled())
855     return Button::OnGestureEvent(event);
856 }
857 
CreateInkDropRipple() const858 std::unique_ptr<views::InkDropRipple> ShelfAppButton::CreateInkDropRipple()
859     const {
860   const gfx::Rect small_ripple_area = CalculateSmallRippleArea();
861   const int ripple_size = shelf_view_->GetShelfItemRippleSize();
862 
863   return std::make_unique<views::SquareInkDropRipple>(
864       gfx::Size(ripple_size, ripple_size), GetInkDropLargeCornerRadius(),
865       small_ripple_area.size(), GetInkDropSmallCornerRadius(),
866       small_ripple_area.CenterPoint(), GetInkDropBaseColor(),
867       GetInkDropVisibleOpacity());
868 }
869 
HandleAccessibleAction(const ui::AXActionData & action_data)870 bool ShelfAppButton::HandleAccessibleAction(
871     const ui::AXActionData& action_data) {
872   if (notification_indicator_ && notification_indicator_->GetVisible())
873     shelf_view_->AnnounceShelfItemNotificationBadge(this);
874 
875   if (action_data.action == ax::mojom::Action::kScrollToMakeVisible)
876     shelf_button_delegate()->HandleAccessibleActionScrollToMakeVisible(this);
877 
878   return views::View::HandleAccessibleAction(action_data);
879 }
880 
InkDropAnimationStarted()881 void ShelfAppButton::InkDropAnimationStarted() {
882   SetInkDropAnimationStarted(/*started=*/true);
883 }
884 
InkDropRippleAnimationEnded(views::InkDropState state)885 void ShelfAppButton::InkDropRippleAnimationEnded(views::InkDropState state) {
886   // Notify the host view of the ink drop to be hidden at the end of ink drop
887   // animation.
888   if (state == views::InkDropState::HIDDEN)
889     SetInkDropAnimationStarted(/*started=*/false);
890 }
891 
UpdateState()892 void ShelfAppButton::UpdateState() {
893   indicator_->SetVisible(!(state_ & STATE_HIDDEN) &&
894                          (state_ & STATE_ATTENTION || state_ & STATE_RUNNING ||
895                           state_ & STATE_ACTIVE));
896 
897   const bool is_horizontal_shelf =
898       shelf_view_->shelf()->IsHorizontalAlignment();
899   indicator_->SetHorizontalShelf(is_horizontal_shelf);
900 
901   icon_view_->SetHorizontalAlignment(
902       is_horizontal_shelf ? views::ImageView::Alignment::kCenter
903                           : views::ImageView::Alignment::kLeading);
904   icon_view_->SetVerticalAlignment(is_horizontal_shelf
905                                        ? views::ImageView::Alignment::kLeading
906                                        : views::ImageView::Alignment::kCenter);
907   SchedulePaint();
908 }
909 
OnTouchDragTimer()910 void ShelfAppButton::OnTouchDragTimer() {
911   AddState(STATE_DRAGGING);
912 }
913 
OnRippleTimer()914 void ShelfAppButton::OnRippleTimer() {
915   if (GetInkDrop()->GetTargetInkDropState() !=
916       views::InkDropState::ACTION_PENDING) {
917     return;
918   }
919   GetInkDrop()->AnimateToState(views::InkDropState::ACTIVATED);
920 }
921 
GetScaleTransform(float icon_scale)922 gfx::Transform ShelfAppButton::GetScaleTransform(float icon_scale) {
923   gfx::RectF pre_scaling_bounds(GetIconViewBounds(1.0f));
924   gfx::RectF target_bounds(GetIconViewBounds(icon_scale));
925   return gfx::TransformBetweenRects(target_bounds, pre_scaling_bounds);
926 }
927 
ScaleAppIcon(bool scale_up)928 void ShelfAppButton::ScaleAppIcon(bool scale_up) {
929   StopObservingImplicitAnimations();
930 
931   if (scale_up) {
932     icon_scale_ = kAppIconScale;
933     SetImage(icon_image_);
934     icon_view_->layer()->SetTransform(GetScaleTransform(kAppIconScale));
935   }
936   ui::ScopedLayerAnimationSettings settings(icon_view_->layer()->GetAnimator());
937   settings.SetTransitionDuration(
938       base::TimeDelta::FromMilliseconds(kDragDropAppIconScaleTransitionMs));
939   if (scale_up) {
940     icon_view_->layer()->SetTransform(gfx::Transform());
941   } else {
942     // To avoid poor quality icons, update icon image with the correct scale
943     // after the transform animation is completed.
944     settings.AddObserver(this);
945     icon_view_->layer()->SetTransform(GetScaleTransform(kAppIconScale));
946   }
947 }
948 
OnImplicitAnimationsCompleted()949 void ShelfAppButton::OnImplicitAnimationsCompleted() {
950   icon_scale_ = 1.0f;
951   SetImage(icon_image_);
952   icon_view_->layer()->SetTransform(gfx::Transform());
953 }
954 
SetInkDropAnimationStarted(bool started)955 void ShelfAppButton::SetInkDropAnimationStarted(bool started) {
956   if (ink_drop_animation_started_ == started)
957     return;
958 
959   ink_drop_animation_started_ = started;
960   if (started) {
961     ink_drop_count_ = shelf_button_delegate()->CreateScopedActiveInkDropCount(
962         /*sender=*/this);
963   } else {
964     ink_drop_count_.reset(nullptr);
965   }
966 }
967 
GetNotificationIndicatorColorForTest()968 SkColor ShelfAppButton::GetNotificationIndicatorColorForTest() {
969   return notification_indicator_->GetColorForTest();
970 }
971 
972 }  // namespace ash
973