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