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