1 // Copyright 2019 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/wm/overview/overview_item_view.h"
6 
7 #include <algorithm>
8 #include <memory>
9 
10 #include "ash/resources/vector_icons/vector_icons.h"
11 #include "ash/strings/grit/ash_strings.h"
12 #include "ash/style/ash_color_provider.h"
13 #include "ash/wm/overview/overview_constants.h"
14 #include "ash/wm/overview/overview_grid.h"
15 #include "ash/wm/overview/overview_item.h"
16 #include "ash/wm/window_preview_view.h"
17 #include "ash/wm/wm_highlight_item_border.h"
18 #include "ui/accessibility/ax_enums.mojom.h"
19 #include "ui/accessibility/ax_node_data.h"
20 #include "ui/aura/window.h"
21 #include "ui/base/l10n/l10n_util.h"
22 #include "ui/compositor/layer.h"
23 #include "ui/compositor/scoped_layer_animation_settings.h"
24 #include "ui/gfx/geometry/size_conversions.h"
25 #include "ui/gfx/image/image_skia_operations.h"
26 #include "ui/gfx/paint_vector_icon.h"
27 #include "ui/strings/grit/ui_strings.h"
28 #include "ui/views/animation/flood_fill_ink_drop_ripple.h"
29 #include "ui/views/animation/ink_drop_impl.h"
30 #include "ui/views/animation/ink_drop_mask.h"
31 #include "ui/views/controls/button/image_button.h"
32 #include "ui/views/controls/highlight_path_generator.h"
33 #include "ui/views/controls/image_view.h"
34 #include "ui/views/controls/label.h"
35 #include "ui/views/widget/widget.h"
36 
37 namespace ash {
38 
39 namespace {
40 
41 // Duration of the show/hide animation of the header.
42 constexpr base::TimeDelta kHeaderFadeDuration =
43     base::TimeDelta::FromMilliseconds(167);
44 
45 // Delay before the show animation of the header.
46 constexpr base::TimeDelta kHeaderFadeInDelay =
47     base::TimeDelta::FromMilliseconds(83);
48 
49 // Duration of the slow show animation of the close button.
50 constexpr base::TimeDelta kCloseButtonSlowFadeInDuration =
51     base::TimeDelta::FromMilliseconds(300);
52 
53 // Delay before the slow show animation of the close button.
54 constexpr base::TimeDelta kCloseButtonSlowFadeInDelay =
55     base::TimeDelta::FromMilliseconds(750);
56 
57 constexpr int kCloseButtonInkDropRadiusDp = 18;
58 
59 // Value should match the one in
60 // ash/resources/vector_icons/overview_window_close.icon.
61 constexpr int kCloseButtonIconMarginDp = 5;
62 
63 // Shadow values for shadow on overview header views.
64 constexpr int kTitleShadowBlur = 28;
65 constexpr SkColor kTitleShadowColor = SkColorSetA(SK_ColorBLACK, 82);
66 constexpr int kIconShadowBlur = 10;
67 constexpr SkColor kIconShadowColor = SkColorSetA(SK_ColorBLACK, 31);
68 
GetTitleShadowValues()69 gfx::ShadowValues GetTitleShadowValues() {
70   return {
71       gfx::ShadowValue(gfx::Vector2d(), kTitleShadowBlur, kTitleShadowColor)};
72 }
73 
GetIconShadowValues()74 gfx::ShadowValues GetIconShadowValues() {
75   return {gfx::ShadowValue(gfx::Vector2d(), kIconShadowBlur, kIconShadowColor)};
76 }
77 
78 // Animates |layer| from 0 -> 1 opacity if |visible| and 1 -> 0 opacity
79 // otherwise. The tween type differs for |visible| and if |visible| is true
80 // there is a slight delay before the animation begins. Does not animate if
81 // opacity matches |visible|.
AnimateLayerOpacity(ui::Layer * layer,bool visible)82 void AnimateLayerOpacity(ui::Layer* layer, bool visible) {
83   float target_opacity = visible ? 1.f : 0.f;
84   if (layer->GetTargetOpacity() == target_opacity)
85     return;
86 
87   layer->SetOpacity(1.f - target_opacity);
88   gfx::Tween::Type tween =
89       visible ? gfx::Tween::LINEAR_OUT_SLOW_IN : gfx::Tween::FAST_OUT_LINEAR_IN;
90   ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
91   settings.SetTransitionDuration(kHeaderFadeDuration);
92   settings.SetTweenType(tween);
93   settings.SetPreemptionStrategy(ui::LayerAnimator::REPLACE_QUEUED_ANIMATIONS);
94   if (visible) {
95     layer->GetAnimator()->SchedulePauseForProperties(
96         kHeaderFadeInDelay, ui::LayerAnimationElement::OPACITY);
97   }
98   layer->SetOpacity(target_opacity);
99 }
100 
101 // The close button for the overview item. It has a custom ink drop.
102 class OverviewCloseButton : public views::ImageButton {
103  public:
OverviewCloseButton(PressedCallback callback)104   explicit OverviewCloseButton(PressedCallback callback)
105       : views::ImageButton(std::move(callback)) {
106     SetInkDropMode(InkDropMode::ON_NO_GESTURE_HANDLER);
107     SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
108     SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
109     SetMinimumImageSize(gfx::Size(kHeaderHeightDp, kHeaderHeightDp));
110     SetAccessibleName(l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE));
111     SetTooltipText(l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE));
112     SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
113 
114     views::InstallFixedSizeCircleHighlightPathGenerator(
115         this, kCloseButtonInkDropRadiusDp);
116   }
117   OverviewCloseButton(const OverviewCloseButton&) = delete;
118   OverviewCloseButton& operator=(const OverviewCloseButton&) = delete;
119   ~OverviewCloseButton() override = default;
120 
121   // Resets the listener so that the listener can go out of scope.
ResetListener()122   void ResetListener() { SetCallback(views::Button::PressedCallback()); }
123 
124  protected:
125   // views::Button:
CreateInkDrop()126   std::unique_ptr<views::InkDrop> CreateInkDrop() override {
127     auto ink_drop = std::make_unique<views::InkDropImpl>(this, size());
128     ink_drop->SetAutoHighlightMode(
129         views::InkDropImpl::AutoHighlightMode::SHOW_ON_RIPPLE);
130     return ink_drop;
131   }
132 
133   // views::ImageButton:
OnThemeChanged()134   void OnThemeChanged() override {
135     views::ImageButton::OnThemeChanged();
136     // Add a shadow to the close vector icon.
137     auto* color_provider = AshColorProvider::Get();
138     const SkColor color = color_provider->GetContentLayerColor(
139         AshColorProvider::ContentLayerType::kButtonIconColor);
140     gfx::ImageSkia image_with_shadow =
141         gfx::ImageSkiaOperations::CreateImageWithDropShadow(
142             gfx::CreateVectorIcon(kOverviewWindowCloseIcon, color),
143             GetIconShadowValues());
144     SetImage(views::Button::STATE_NORMAL, image_with_shadow);
145 
146     const auto ripple_attributes = color_provider->GetRippleAttributes(color);
147     SetInkDropBaseColor(ripple_attributes.base_color);
148     SetInkDropVisibleOpacity(ripple_attributes.inkdrop_opacity);
149   }
150 };
151 
152 }  // namespace
153 
OverviewItemView(OverviewItem * overview_item,views::Button::PressedCallback close_callback,aura::Window * window,bool show_preview)154 OverviewItemView::OverviewItemView(
155     OverviewItem* overview_item,
156     views::Button::PressedCallback close_callback,
157     aura::Window* window,
158     bool show_preview)
159     : WindowMiniView(window), overview_item_(overview_item) {
160   DCHECK(overview_item_);
161   // This should not be focusable. It's also to avoid accessibility error when
162   // |window->GetTitle()| is empty.
163   SetFocusBehavior(FocusBehavior::NEVER);
164 
165   close_button_ = header_view()->AddChildView(
166       std::make_unique<OverviewCloseButton>(std::move(close_callback)));
167   close_button_->SetPaintToLayer();
168   close_button_->layer()->SetFillsBoundsOpaquely(false);
169   // The button's image may be larger than |kHeaderHeightDp| due to added
170   // shadows.
171   close_button_->SetPreferredSize(gfx::Size(kHeaderHeightDp, kHeaderHeightDp));
172 
173   title_label()->SetShadows(GetTitleShadowValues());
174 
175   // Call this last as it calls |Layout()| which relies on the some of the other
176   // elements existing.
177   SetShowPreview(show_preview);
178   // Do not show header if the current overview item is the drop target widget.
179   if (show_preview || overview_item_->overview_grid()->IsDropTargetWindow(
180                           overview_item_->GetWindow())) {
181     header_view()->layer()->SetOpacity(0.f);
182     current_header_visibility_ = HeaderVisibility::kInvisible;
183   }
184 
185   UpdateIconView();
186 
187   // Do not use a layout manager for the header as its elements have shadows
188   // which need to overlap each other. Remove the FlexLayout set in
189   // WindowMiniView.
190   header_view()->SetLayoutManager(nullptr);
191 }
192 
193 OverviewItemView::~OverviewItemView() = default;
194 
SetHeaderVisibility(HeaderVisibility visibility)195 void OverviewItemView::SetHeaderVisibility(HeaderVisibility visibility) {
196   DCHECK(header_view()->layer());
197   if (visibility == current_header_visibility_)
198     return;
199   const HeaderVisibility previous_visibility = current_header_visibility_;
200   current_header_visibility_ = visibility;
201 
202   const bool all_invisible = visibility == HeaderVisibility::kInvisible;
203   AnimateLayerOpacity(header_view()->layer(), !all_invisible);
204 
205   // If |header_view()| is fading out, we are done. Depending on if the close
206   // button was visible, it will fade out with |header_view()| or stay hidden.
207   if (all_invisible || !close_button_)
208     return;
209 
210   const bool close_button_visible = visibility == HeaderVisibility::kVisible;
211   // If |header_view()| was hidden and is fading in, set the opacity of
212   // |close_button_| depending on whether the close button should fade in with
213   // |header_view()| or stay hidden.
214   if (previous_visibility == HeaderVisibility::kInvisible) {
215     close_button_->layer()->SetOpacity(close_button_visible ? 1.f : 0.f);
216     return;
217   }
218 
219   AnimateLayerOpacity(close_button_->layer(), close_button_visible);
220 }
221 
HideCloseInstantlyAndThenShowItSlowly()222 void OverviewItemView::HideCloseInstantlyAndThenShowItSlowly() {
223   DCHECK(close_button_);
224   DCHECK_NE(HeaderVisibility::kInvisible, current_header_visibility_);
225   ui::Layer* layer = close_button_->layer();
226   DCHECK(layer);
227   current_header_visibility_ = HeaderVisibility::kVisible;
228   layer->SetOpacity(0.f);
229   ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
230   settings.SetTransitionDuration(kCloseButtonSlowFadeInDuration);
231   settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
232   settings.SetPreemptionStrategy(ui::LayerAnimator::REPLACE_QUEUED_ANIMATIONS);
233   layer->GetAnimator()->SchedulePauseForProperties(
234       kCloseButtonSlowFadeInDelay, ui::LayerAnimationElement::OPACITY);
235   layer->SetOpacity(1.f);
236 }
237 
OnOverviewItemWindowRestoring()238 void OverviewItemView::OnOverviewItemWindowRestoring() {
239   overview_item_ = nullptr;
240   static_cast<OverviewCloseButton*>(close_button_)->ResetListener();
241 }
242 
RefreshPreviewView()243 void OverviewItemView::RefreshPreviewView() {
244   if (!preview_view())
245     return;
246 
247   preview_view()->RecreatePreviews();
248   Layout();
249 }
250 
GetHeaderBounds() const251 gfx::Rect OverviewItemView::GetHeaderBounds() const {
252   // We want to align the edges of the image as shown below in the diagram. The
253   // resource itself contains some padding, which is the distance from the edges
254   // of the image to the edges of the vector icon. The icon keeps its size in
255   // dips and is centered in the middle of the preferred width, so the
256   // additional padding would be equal to half the difference in width between
257   // the preferred width and the image size. The resulting padding would be that
258   // number plus the padding in the resource, in dips.
259   const int image_width = kIconSize.width();
260   const int close_button_width = close_button_->GetPreferredSize().width();
261   const int right_padding =
262       (close_button_width - image_width) / 2 + kCloseButtonIconMarginDp;
263 
264   // Positions the header in a way so that the right aligned close button is
265   // aligned so that the edge of its icon, not the button lines up with the
266   // margins. In the diagram below, a represents the the right edge of the
267   // provided icon (which contains some padding), b represents the right edge of
268   // |close_button_| and c represents the right edge of the local bounds.
269   // ---------------------------+---------+
270   //                            |  +---+  |
271   //      |title_label_|        |  |   a  b
272   //                            |  +---+  |
273   // ---------------------------+---------+
274   //                                   c
275   //                                   |
276 
277   // The size of this view is larger than that of the visible elements. Position
278   // the header so that the margin is accounted for, then shift the right bounds
279   // by a bit so that the close button resource lines up with the right edge of
280   // the visible region.
281   const gfx::Rect contents_bounds(GetContentsBounds());
282   return gfx::Rect(contents_bounds.x(), contents_bounds.y(),
283                    contents_bounds.width() + right_padding, kHeaderHeightDp);
284 }
285 
GetPreviewViewSize() const286 gfx::Size OverviewItemView::GetPreviewViewSize() const {
287   // The preview should expand to fit the bounds allocated for the content,
288   // except if it is letterboxed or pillarboxed.
289   const gfx::SizeF preview_pref_size(preview_view()->GetPreferredSize());
290   const float aspect_ratio =
291       preview_pref_size.width() / preview_pref_size.height();
292   gfx::SizeF target_size(GetContentAreaBounds().size());
293   OverviewGridWindowFillMode fill_mode =
294       overview_item_ ? overview_item_->GetWindowDimensionsType()
295                      : OverviewGridWindowFillMode::kNormal;
296   switch (fill_mode) {
297     case OverviewGridWindowFillMode::kNormal:
298       break;
299     case OverviewGridWindowFillMode::kLetterBoxed:
300       target_size.set_height(target_size.width() / aspect_ratio);
301       break;
302     case OverviewGridWindowFillMode::kPillarBoxed:
303       target_size.set_width(target_size.height() * aspect_ratio);
304       break;
305   }
306 
307   return gfx::ToRoundedSize(target_size);
308 }
309 
ModifyIcon(gfx::ImageSkia * image) const310 gfx::ImageSkia OverviewItemView::ModifyIcon(gfx::ImageSkia* image) const {
311   gfx::ImageSkia image_resized = gfx::ImageSkiaOperations::CreateResizedImage(
312       *image, skia::ImageOperations::RESIZE_BEST, kIconSize);
313   return gfx::ImageSkiaOperations::CreateImageWithDropShadow(
314       image_resized, GetIconShadowValues());
315 }
316 
Layout()317 void OverviewItemView::Layout() {
318   WindowMiniView::Layout();
319 
320   // Layout the header items. The icon, if available should be aligned left, the
321   // close button should be aligned right and the title should take up all the
322   // space in between but the text should be aligned left.
323   gfx::Rect header_bounds = header_view()->bounds();
324   const int width = header_bounds.width();
325   const int height = header_bounds.height();
326   int x = 0;
327   if (icon_view()) {
328     const int icon_width = kIconSize.width();
329     icon_view()->SetBounds(x, 0, icon_width, height);
330     x += icon_width;
331   }
332   const gfx::Size close_button_size = close_button()->GetPreferredSize();
333   close_button()->SetBoundsRect(gfx::Rect(
334       gfx::Point(width - close_button_size.width(), 0), close_button_size));
335 
336   // The title label text has shadow blur of |kTitleShadowBlur|. This will cause
337   // the preferred size of the title label to increase by |kTitleShadowBlur| / 2
338   // on all sides. To create the visual of the title label being
339   // |kHeaderPaddingDp| away from the icon (excluding the shadows), layout it
340   // somewhat on top of the icon.
341   x -= (kTitleShadowBlur / 2 - kHeaderPaddingDp);
342   title_label()->SetBounds(x, 0, width - close_button_size.width() - x, height);
343 }
344 
GetView()345 views::View* OverviewItemView::GetView() {
346   return this;
347 }
348 
MaybeActivateHighlightedView()349 void OverviewItemView::MaybeActivateHighlightedView() {
350   if (overview_item_)
351     overview_item_->OnHighlightedViewActivated();
352 }
353 
MaybeCloseHighlightedView()354 void OverviewItemView::MaybeCloseHighlightedView() {
355   if (overview_item_)
356     overview_item_->OnHighlightedViewClosed();
357 }
358 
OnViewHighlighted()359 void OverviewItemView::OnViewHighlighted() {
360   UpdateBorderState(/*show=*/true);
361 }
362 
OnViewUnhighlighted()363 void OverviewItemView::OnViewUnhighlighted() {
364   UpdateBorderState(/*show=*/false);
365 }
366 
GetMagnifierFocusPointInScreen()367 gfx::Point OverviewItemView::GetMagnifierFocusPointInScreen() {
368   // When this item is tabbed into, put the magnifier focus on the front of the
369   // title, so that users can read the title first thing.
370   const gfx::Rect title_bounds = title_label()->GetBoundsInScreen();
371   return gfx::Point(GetMirroredXInView(title_bounds.x()),
372                     title_bounds.CenterPoint().y());
373 }
374 
GetClassName() const375 const char* OverviewItemView::GetClassName() const {
376   return "OverviewItemView";
377 }
378 
OnMousePressed(const ui::MouseEvent & event)379 bool OverviewItemView::OnMousePressed(const ui::MouseEvent& event) {
380   if (!overview_item_)
381     return views::View::OnMousePressed(event);
382   overview_item_->HandleMouseEvent(event);
383   return true;
384 }
385 
OnMouseDragged(const ui::MouseEvent & event)386 bool OverviewItemView::OnMouseDragged(const ui::MouseEvent& event) {
387   if (!overview_item_)
388     return views::View::OnMouseDragged(event);
389   overview_item_->HandleMouseEvent(event);
390   return true;
391 }
392 
OnMouseReleased(const ui::MouseEvent & event)393 void OverviewItemView::OnMouseReleased(const ui::MouseEvent& event) {
394   if (!overview_item_) {
395     views::View::OnMouseReleased(event);
396     return;
397   }
398   overview_item_->HandleMouseEvent(event);
399 }
400 
OnGestureEvent(ui::GestureEvent * event)401 void OverviewItemView::OnGestureEvent(ui::GestureEvent* event) {
402   if (!overview_item_)
403     return;
404 
405   overview_item_->HandleGestureEvent(event);
406   event->SetHandled();
407 }
408 
CanAcceptEvent(const ui::Event & event)409 bool OverviewItemView::CanAcceptEvent(const ui::Event& event) {
410   bool accept_events = true;
411   // Do not process or accept press down events that are on the border.
412   static ui::EventType press_types[] = {ui::ET_GESTURE_TAP_DOWN,
413                                         ui::ET_MOUSE_PRESSED};
414   if (event.IsLocatedEvent() && base::Contains(press_types, event.type())) {
415     const gfx::Rect content_bounds = GetContentsBounds();
416     if (!content_bounds.Contains(event.AsLocatedEvent()->location()))
417       accept_events = false;
418   }
419 
420   return accept_events && views::View::CanAcceptEvent(event);
421 }
422 
GetAccessibleNodeData(ui::AXNodeData * node_data)423 void OverviewItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
424   WindowMiniView::GetAccessibleNodeData(node_data);
425 
426   // TODO: This doesn't allow |this| to be navigated by ChromeVox, find a way
427   // to allow |this| as well as the title and close button.
428   node_data->role = ax::mojom::Role::kGenericContainer;
429   node_data->AddStringAttribute(
430       ax::mojom::StringAttribute::kDescription,
431       l10n_util::GetStringUTF8(
432           IDS_ASH_OVERVIEW_CLOSABLE_HIGHLIGHT_ITEM_A11Y_EXTRA_TIP));
433 }
434 
435 }  // namespace ash
436