1 // Copyright 2016 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/system/toast/toast_overlay.h"
6
7 #include "ash/keyboard/ui/keyboard_ui_controller.h"
8 #include "ash/public/cpp/ash_typography.h"
9 #include "ash/public/cpp/shell_window_ids.h"
10 #include "ash/resources/vector_icons/vector_icons.h"
11 #include "ash/root_window_controller.h"
12 #include "ash/shell.h"
13 #include "ash/strings/grit/ash_strings.h"
14 #include "ash/style/ash_color_provider.h"
15 #include "ash/wm/work_area_insets.h"
16 #include "base/strings/string_util.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "base/threading/thread_task_runner_handle.h"
19 #include "ui/accessibility/ax_enums.mojom.h"
20 #include "ui/base/l10n/l10n_util.h"
21 #include "ui/display/display_observer.h"
22 #include "ui/gfx/canvas.h"
23 #include "ui/gfx/font_list.h"
24 #include "ui/gfx/geometry/insets.h"
25 #include "ui/gfx/paint_vector_icon.h"
26 #include "ui/views/animation/ink_drop_highlight.h"
27 #include "ui/views/background.h"
28 #include "ui/views/border.h"
29 #include "ui/views/controls/button/label_button.h"
30 #include "ui/views/controls/highlight_path_generator.h"
31 #include "ui/views/controls/label.h"
32 #include "ui/views/layout/box_layout.h"
33 #include "ui/views/view.h"
34 #include "ui/views/widget/widget.h"
35 #include "ui/views/widget/widget_delegate.h"
36 #include "ui/wm/core/window_animations.h"
37
38 namespace ash {
39
40 namespace {
41
42 // Duration of slide animation when overlay is shown or hidden.
43 constexpr int kSlideAnimationDurationMs = 100;
44
45 // These values are in DIP.
46 constexpr int kToastCornerRounding = 16;
47 constexpr int kToastHeight = 32;
48 constexpr int kToastHorizontalSpacing = 16;
49 constexpr int kToastMaximumWidth = 512;
50 constexpr int kToastMinimumWidth = 288;
51 constexpr int kToastButtonMaximumWidth = 160;
52
53 // Returns the work area bounds for the root window where new windows are added
54 // (including new toasts).
GetUserWorkAreaBounds()55 gfx::Rect GetUserWorkAreaBounds() {
56 return WorkAreaInsets::ForWindow(Shell::GetRootWindowForNewWindows())
57 ->user_work_area_bounds();
58 }
59
60 ///////////////////////////////////////////////////////////////////////////////
61 // ToastOverlayLabel
62 class ToastOverlayLabel : public views::Label {
63 public:
ToastOverlayLabel(const base::string16 & label)64 explicit ToastOverlayLabel(const base::string16& label)
65 : Label(label, CONTEXT_TOAST_OVERLAY) {
66 SetHorizontalAlignment(gfx::ALIGN_LEFT);
67 SetAutoColorReadabilityEnabled(false);
68 SetMultiLine(true);
69 SetMaxLines(2);
70 SetSubpixelRenderingEnabled(false);
71
72 int vertical_spacing =
73 std::max((kToastHeight - GetPreferredSize().height()) / 2, 0);
74 SetBorder(views::CreateEmptyBorder(
75 gfx::Insets(vertical_spacing, kToastHorizontalSpacing)));
76 }
77
78 ToastOverlayLabel(const ToastOverlayLabel&) = delete;
79 ToastOverlayLabel& operator=(const ToastOverlayLabel&) = delete;
80 ~ToastOverlayLabel() override = default;
81
82 private:
83 // views::Label:
OnThemeChanged()84 void OnThemeChanged() override {
85 views::Label::OnThemeChanged();
86 SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
87 AshColorProvider::ContentLayerType::kTextColorPrimary));
88 }
89 };
90
91 } // namespace
92
93 ///////////////////////////////////////////////////////////////////////////////
94 // ToastDisplayObserver
95 class ToastOverlay::ToastDisplayObserver : public display::DisplayObserver {
96 public:
ToastDisplayObserver(ToastOverlay * overlay)97 ToastDisplayObserver(ToastOverlay* overlay) : overlay_(overlay) {
98 display::Screen::GetScreen()->AddObserver(this);
99 }
100
~ToastDisplayObserver()101 ~ToastDisplayObserver() override {
102 display::Screen::GetScreen()->RemoveObserver(this);
103 }
104
OnDisplayMetricsChanged(const display::Display & display,uint32_t changed_metrics)105 void OnDisplayMetricsChanged(const display::Display& display,
106 uint32_t changed_metrics) override {
107 overlay_->UpdateOverlayBounds();
108 }
109
110 private:
111 ToastOverlay* const overlay_;
112 DISALLOW_COPY_AND_ASSIGN(ToastDisplayObserver);
113 };
114
115 ///////////////////////////////////////////////////////////////////////////////
116 // ToastOverlayButton
117 class ToastOverlayButton : public views::LabelButton {
118 public:
ToastOverlayButton(PressedCallback callback,const base::string16 & text)119 ToastOverlayButton(PressedCallback callback, const base::string16& text)
120 : views::LabelButton(std::move(callback), text, CONTEXT_TOAST_OVERLAY) {
121 SetInkDropMode(InkDropMode::ON);
122 SetHasInkDropActionOnClick(true);
123
124 // Treat the space below the baseline as a margin.
125 int vertical_spacing =
126 std::max((kToastHeight - GetPreferredSize().height()) / 2, 0);
127 SetBorder(views::CreateEmptyBorder(
128 gfx::Insets(vertical_spacing, kToastHorizontalSpacing)));
129
130 views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
131 kToastCornerRounding);
132 SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
133 }
134
135 ToastOverlayButton(const ToastOverlayButton&) = delete;
136 ToastOverlayButton& operator=(const ToastOverlayButton&) = delete;
137 ~ToastOverlayButton() override = default;
138
139 protected:
140 // views::LabelButton:
CreateInkDropHighlight() const141 std::unique_ptr<views::InkDropHighlight> CreateInkDropHighlight()
142 const override {
143 return std::make_unique<views::InkDropHighlight>(
144 gfx::SizeF(GetLocalBounds().size()), GetInkDropBaseColor());
145 }
146
147 private:
148 friend class ToastOverlay; // for ToastOverlay::ClickDismissButtonForTesting.
149
150 // views::LabelButton:
OnThemeChanged()151 void OnThemeChanged() override {
152 views::LabelButton::OnThemeChanged();
153 const auto* color_provider = AshColorProvider::Get();
154 SetInkDropBaseColor(color_provider->GetRippleAttributes().base_color);
155 SetEnabledTextColors(color_provider->GetContentLayerColor(
156 AshColorProvider::ContentLayerType::kButtonLabelColorBlue));
157 }
158 };
159
160 ///////////////////////////////////////////////////////////////////////////////
161 // ToastOverlayView
162 class ToastOverlayView : public views::View {
163 public:
164 // This object is not owned by the views hierarchy or by the widget.
ToastOverlayView(ToastOverlay * overlay,const base::string16 & text,const base::Optional<base::string16> & dismiss_text,const bool is_managed)165 ToastOverlayView(ToastOverlay* overlay,
166 const base::string16& text,
167 const base::Optional<base::string16>& dismiss_text,
168 const bool is_managed) {
169 SetPaintToLayer();
170 layer()->SetFillsBoundsOpaquely(false);
171 layer()->SetRoundedCornerRadius(gfx::RoundedCornersF(kToastCornerRounding));
172 layer()->SetBackgroundBlur(
173 static_cast<float>(AshColorProvider::LayerBlurSigma::kBlurDefault));
174
175 auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
176 views::BoxLayout::Orientation::kHorizontal));
177
178 int icon_width = 0;
179 if (is_managed) {
180 managed_icon_ = AddChildView(std::make_unique<views::ImageView>());
181 managed_icon_->SetBorder(views::CreateEmptyBorder(
182 gfx::Insets(kToastHorizontalSpacing, kToastHorizontalSpacing,
183 kToastHorizontalSpacing, /*right=*/0)));
184 icon_width =
185 managed_icon_->GetPreferredSize().width() + kToastHorizontalSpacing;
186 }
187
188 auto* label = AddChildView(std::make_unique<ToastOverlayLabel>(text));
189 label->SetMaximumWidth(GetMaximumSize().width() - icon_width);
190 layout->SetFlexForView(label, 1);
191
192 if (!dismiss_text.has_value())
193 return;
194
195 button_ = AddChildView(std::make_unique<ToastOverlayButton>(
196 base::BindRepeating(&ToastOverlay::Show, base::Unretained(overlay),
197 false),
198 dismiss_text.value().empty()
199 ? l10n_util::GetStringUTF16(IDS_ASH_TOAST_DISMISS_BUTTON)
200 : dismiss_text.value()));
201
202 const int button_width =
203 std::min(button_->GetPreferredSize().width(), kToastButtonMaximumWidth);
204 button_->SetMaxSize(gfx::Size(button_width, GetMaximumSize().height()));
205 label->SetMaximumWidth(GetMaximumSize().width() - button_width -
206 icon_width - kToastHorizontalSpacing * 2 -
207 kToastHorizontalSpacing * 2);
208 }
209
210 ToastOverlayView(const ToastOverlayView&) = delete;
211 ToastOverlayView& operator=(const ToastOverlayView&) = delete;
212 ~ToastOverlayView() override = default;
213
button()214 ToastOverlayButton* button() { return button_; }
215
216 private:
217 // views::View:
GetMinimumSize() const218 gfx::Size GetMinimumSize() const override {
219 return gfx::Size(kToastMinimumWidth, kToastHeight);
220 }
221
GetMaximumSize() const222 gfx::Size GetMaximumSize() const override {
223 return gfx::Size(kToastMaximumWidth, GetUserWorkAreaBounds().height() -
224 ToastOverlay::kOffset * 2);
225 }
226
OnThemeChanged()227 void OnThemeChanged() override {
228 views::View::OnThemeChanged();
229 auto* color_provider = AshColorProvider::Get();
230 SetBackground(
231 views::CreateSolidBackground(color_provider->GetBaseLayerColor(
232 AshColorProvider::BaseLayerType::kTransparent80)));
233 if (managed_icon_) {
234 managed_icon_->SetImage(gfx::CreateVectorIcon(
235 kUnifiedMenuManagedIcon,
236 color_provider->GetContentLayerColor(
237 AshColorProvider::ContentLayerType::kIconColorPrimary)));
238 }
239 }
240
241 ToastOverlayButton* button_ = nullptr; // weak
242 views::ImageView* managed_icon_ = nullptr;
243 };
244
245 ///////////////////////////////////////////////////////////////////////////////
246 // ToastOverlay
ToastOverlay(Delegate * delegate,const base::string16 & text,base::Optional<base::string16> dismiss_text,bool show_on_lock_screen,bool is_managed)247 ToastOverlay::ToastOverlay(Delegate* delegate,
248 const base::string16& text,
249 base::Optional<base::string16> dismiss_text,
250 bool show_on_lock_screen,
251 bool is_managed)
252 : delegate_(delegate),
253 text_(text),
254 dismiss_text_(dismiss_text),
255 overlay_widget_(new views::Widget),
256 overlay_view_(new ToastOverlayView(this, text, dismiss_text, is_managed)),
257 display_observer_(std::make_unique<ToastDisplayObserver>(this)),
258 widget_size_(overlay_view_->GetPreferredSize()) {
259 views::Widget::InitParams params;
260 params.type = views::Widget::InitParams::TYPE_POPUP;
261 params.name = "ToastOverlay";
262 params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
263 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
264 params.accept_events = true;
265 params.z_order = ui::ZOrderLevel::kFloatingUIElement;
266 params.bounds = CalculateOverlayBounds();
267 // Show toasts above the app list and below the lock screen.
268 params.parent = Shell::GetRootWindowForNewWindows()->GetChildById(
269 show_on_lock_screen ? kShellWindowId_LockSystemModalContainer
270 : kShellWindowId_SystemModalContainer);
271 overlay_widget_->Init(std::move(params));
272 overlay_widget_->SetVisibilityChangedAnimationsEnabled(true);
273 overlay_widget_->SetContentsView(overlay_view_.get());
274 UpdateOverlayBounds();
275
276 aura::Window* overlay_window = overlay_widget_->GetNativeWindow();
277 ::wm::SetWindowVisibilityAnimationType(
278 overlay_window, ::wm::WINDOW_VISIBILITY_ANIMATION_TYPE_VERTICAL);
279 ::wm::SetWindowVisibilityAnimationDuration(
280 overlay_window,
281 base::TimeDelta::FromMilliseconds(kSlideAnimationDurationMs));
282
283 keyboard::KeyboardUIController::Get()->AddObserver(this);
284 }
285
~ToastOverlay()286 ToastOverlay::~ToastOverlay() {
287 keyboard::KeyboardUIController::Get()->RemoveObserver(this);
288 overlay_widget_->Close();
289 }
290
Show(bool visible)291 void ToastOverlay::Show(bool visible) {
292 if (overlay_widget_->GetLayer()->GetTargetVisibility() == visible)
293 return;
294
295 ui::LayerAnimator* animator = overlay_widget_->GetLayer()->GetAnimator();
296 DCHECK(animator);
297
298 base::TimeDelta original_duration = animator->GetTransitionDuration();
299 ui::ScopedLayerAnimationSettings animation_settings(animator);
300 // ScopedLayerAnimationSettings ctor changes the transition duration, so
301 // change it back to the original value (should be zero).
302 animation_settings.SetTransitionDuration(original_duration);
303
304 animation_settings.AddObserver(this);
305
306 if (visible) {
307 overlay_widget_->Show();
308
309 // Notify accessibility about the overlay.
310 overlay_view_->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, false);
311 } else {
312 overlay_widget_->Hide();
313 }
314 }
315
UpdateOverlayBounds()316 void ToastOverlay::UpdateOverlayBounds() {
317 overlay_widget_->SetBounds(CalculateOverlayBounds());
318 }
319
CalculateOverlayBounds()320 gfx::Rect ToastOverlay::CalculateOverlayBounds() {
321 gfx::Rect bounds = GetUserWorkAreaBounds();
322 int target_y =
323 bounds.bottom() - widget_size_.height() - ToastOverlay::kOffset;
324 bounds.ClampToCenteredSize(widget_size_);
325 bounds.set_y(target_y);
326 return bounds;
327 }
328
OnImplicitAnimationsScheduled()329 void ToastOverlay::OnImplicitAnimationsScheduled() {}
330
OnImplicitAnimationsCompleted()331 void ToastOverlay::OnImplicitAnimationsCompleted() {
332 if (!overlay_widget_->GetLayer()->GetTargetVisibility())
333 delegate_->OnClosed();
334 }
335
OnKeyboardOccludedBoundsChanged(const gfx::Rect & new_bounds_in_screen)336 void ToastOverlay::OnKeyboardOccludedBoundsChanged(
337 const gfx::Rect& new_bounds_in_screen) {
338 // TODO(https://crbug.com/943446): Observe changes in user work area bounds
339 // directly instead of listening for keyboard bounds changes.
340 UpdateOverlayBounds();
341 }
342
widget_for_testing()343 views::Widget* ToastOverlay::widget_for_testing() {
344 return overlay_widget_.get();
345 }
346
dismiss_button_for_testing()347 ToastOverlayButton* ToastOverlay::dismiss_button_for_testing() {
348 return overlay_view_->button();
349 }
350
ClickDismissButtonForTesting(const ui::Event & event)351 void ToastOverlay::ClickDismissButtonForTesting(const ui::Event& event) {
352 DCHECK(overlay_view_->button());
353 overlay_view_->button()->NotifyClick(event);
354 }
355
356 } // namespace ash
357