1 // Copyright (c) 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.
5 #include "ui/message_center/views/toast_contents_view.h"
10 #include "base/compiler_specific.h"
11 #include "base/memory/weak_ptr.h"
12 #include "base/time/time.h"
13 #include "build/build_config.h"
14 #include "ui/accessibility/ax_enums.mojom.h"
15 #include "ui/accessibility/ax_node_data.h"
16 #include "ui/aura/window.h"
17 #include "ui/aura/window_targeter.h"
18 #include "ui/display/display.h"
19 #include "ui/display/screen.h"
20 #include "ui/gfx/animation/animation_delegate.h"
21 #include "ui/gfx/animation/slide_animation.h"
22 #include "ui/message_center/public/cpp/message_center_constants.h"
23 #include "ui/message_center/public/cpp/notification.h"
24 #include "ui/message_center/views/message_popup_collection.h"
25 #include "ui/message_center/views/message_view.h"
26 #include "ui/message_center/views/popup_alignment_delegate.h"
27 #include "ui/views/background.h"
28 #include "ui/views/view.h"
29 #include "ui/views/widget/widget.h"
30 #include "ui/views/widget/widget_delegate.h"
33 #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
36 using display::Screen;
38 namespace message_center {
41 // The width of a toast before animated reveal and after closing.
42 const int kClosedToastWidth = 5;
44 // FadeIn/Out look a bit better if they are slightly longer then default slide.
45 const int kFadeInOutDuration = 200;
50 const char ToastContentsView::kViewClassName[] = "ToastContentsView";
53 gfx::Size ToastContentsView::GetToastSizeForView(const views::View* view) {
54 int width = kNotificationWidth + view->GetInsets().width();
55 return gfx::Size(width, view->GetHeightForWidth(width));
58 ToastContentsView::ToastContentsView(
59 const std::string& notification_id,
60 PopupAlignmentDelegate* alignment_delegate,
61 base::WeakPtr<MessagePopupCollection> collection)
62 : collection_(collection),
65 closing_animation_(NULL) {
66 DCHECK(alignment_delegate);
67 set_notify_enter_exit_on_child(true);
68 // Sets the transparent background. Then, when the message view is slid out,
69 // the whole toast seems to slide although the actual bound of the widget
70 // remains. This is hacky but easier to keep the consistency.
71 SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
73 fade_animation_.reset(new gfx::SlideAnimation(this));
74 fade_animation_->SetSlideDuration(kFadeInOutDuration);
76 CreateWidget(alignment_delegate);
79 // This is destroyed when the toast window closes.
80 ToastContentsView::~ToastContentsView() {
82 collection_->ForgetToast(this);
85 void ToastContentsView::SetContents(MessageView* view,
86 bool a11y_feedback_for_updates) {
88 RemoveAllChildViews(true);
90 UpdatePreferredSize();
91 if (a11y_feedback_for_updates)
92 NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
95 void ToastContentsView::UpdateContents(const Notification& notification,
96 bool a11y_feedback_for_updates) {
97 DCHECK_GT(child_count(), 0);
98 MessageView* message_view = static_cast<MessageView*>(child_at(0));
99 message_view->UpdateWithNotification(notification);
100 UpdatePreferredSize();
101 if (a11y_feedback_for_updates)
102 NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
105 void ToastContentsView::RevealWithAnimation(gfx::Point origin) {
106 // Place/move the toast widgets. Currently it stacks the widgets from the
107 // right-bottom of the work area.
108 // TODO(mukai): allow to specify the placement policy from outside of this
109 // class. The policy should be specified from preference on Windows, or
110 // the launcher alignment on ChromeOS.
111 origin_ = gfx::Point(origin.x() - preferred_size_.width(),
112 origin.y() - preferred_size_.height());
114 gfx::Rect stable_bounds(origin_, preferred_size_);
116 SetBoundsInstantly(GetClosedToastBounds(stable_bounds));
118 SetBoundsWithAnimation(stable_bounds);
121 void ToastContentsView::CloseWithAnimation() {
128 void ToastContentsView::SetBoundsInstantly(gfx::Rect new_bounds) {
129 DCHECK(new_bounds.size().width() <= preferred_size_.width() &&
130 new_bounds.size().height() <= preferred_size_.height())
131 << "we can not display widget bigger than notification";
136 if (new_bounds == GetWidget()->GetWindowBoundsInScreen())
139 origin_ = new_bounds.origin();
140 GetWidget()->SetBounds(new_bounds);
143 void ToastContentsView::SetBoundsWithAnimation(gfx::Rect new_bounds) {
144 DCHECK(new_bounds.size().width() <= preferred_size_.width() &&
145 new_bounds.size().height() <= preferred_size_.height())
146 << "we can not display widget bigger than notification";
151 if (new_bounds == animated_bounds_end_)
154 origin_ = new_bounds.origin();
155 // This picks up the current bounds, so if there was a previous animation
156 // half-done, the next one will pick up from the current location.
157 // This is the only place that should query current location of the Widget
158 // on screen, the rest should refer to the bounds_.
159 animated_bounds_start_ = GetWidget()->GetWindowBoundsInScreen();
160 animated_bounds_end_ = new_bounds;
162 if (bounds_animation_.get())
163 bounds_animation_->Stop();
165 bounds_animation_.reset(new gfx::SlideAnimation(this));
166 bounds_animation_->Show();
169 void ToastContentsView::StartFadeIn() {
170 fade_animation_->Stop();
172 GetWidget()->SetOpacity(0);
173 GetWidget()->ShowInactive();
174 fade_animation_->Reset(0);
175 fade_animation_->Show();
178 void ToastContentsView::StartFadeOut() {
179 fade_animation_->Stop();
181 closing_animation_ = (is_closing_ ? fade_animation_.get() : nullptr);
182 if (GetWidget()->GetLayer()->opacity() > 0.0) {
183 fade_animation_->Reset(1);
184 fade_animation_->Hide();
186 // If the layer is already transparent, do not trigger animation again.
187 // It happens when the toast is removed by touch gesture.
188 OnBoundsAnimationEndedOrCancelled(fade_animation_.get());
192 void ToastContentsView::OnBoundsAnimationEndedOrCancelled(
193 const gfx::Animation* animation) {
194 if (is_closing_ && closing_animation_ == animation && GetWidget()) {
195 views::Widget* widget = GetWidget();
197 // TODO(dewittj): This is a workaround to prevent a nasty bug where
198 // closing a transparent widget doesn't actually remove the window,
199 // causing entire areas of the screen to become unresponsive to clicks.
200 // See crbug.com/243469
203 widget->SetOpacity(1.f);
210 // gfx::AnimationDelegate
211 void ToastContentsView::AnimationProgressed(const gfx::Animation* animation) {
212 if (animation == bounds_animation_.get()) {
213 gfx::Rect current(animation->CurrentValueBetween(
214 animated_bounds_start_, animated_bounds_end_));
215 GetWidget()->SetBounds(current);
216 } else if (animation == fade_animation_.get()) {
217 GetWidget()->SetOpacity(
218 static_cast<float>(fade_animation_->GetCurrentValue()));
222 void ToastContentsView::AnimationEnded(const gfx::Animation* animation) {
223 OnBoundsAnimationEndedOrCancelled(animation);
226 void ToastContentsView::AnimationCanceled(
227 const gfx::Animation* animation) {
228 OnBoundsAnimationEndedOrCancelled(animation);
231 // views::WidgetDelegate
232 void ToastContentsView::WindowClosing() {
233 if (!is_closing_ && collection_)
234 collection_->ForgetToast(this);
237 void ToastContentsView::OnDisplayChanged() {
238 views::Widget* widget = GetWidget();
242 gfx::NativeView native_view = widget->GetNativeView();
243 if (!native_view || !collection_.get())
246 collection_->OnDisplayMetricsChanged(
247 Screen::GetScreen()->GetDisplayNearestView(native_view));
250 void ToastContentsView::OnWorkAreaChanged() {
251 views::Widget* widget = GetWidget();
255 gfx::NativeView native_view = widget->GetNativeView();
256 if (!native_view || !collection_.get())
259 collection_->OnDisplayMetricsChanged(
260 Screen::GetScreen()->GetDisplayNearestView(native_view));
263 void ToastContentsView::OnWidgetActivationChanged(views::Widget* widget,
266 collection_->PausePopupTimers();
268 collection_->RestartPopupTimers();
272 void ToastContentsView::OnMouseEntered(const ui::MouseEvent& event) {
274 collection_->OnMouseEntered(this);
277 void ToastContentsView::OnMouseExited(const ui::MouseEvent& event) {
279 collection_->OnMouseExited(this);
282 void ToastContentsView::Layout() {
283 if (child_count() > 0) {
284 child_at(0)->SetBounds(
285 0, 0, preferred_size_.width(), preferred_size_.height());
289 gfx::Size ToastContentsView::CalculatePreferredSize() const {
290 return child_count() ? GetToastSizeForView(child_at(0)) : gfx::Size();
293 void ToastContentsView::UpdatePreferredSize() {
294 DCHECK_GT(child_count(), 0);
295 gfx::Size new_size = GetToastSizeForView(child_at(0));
296 if (preferred_size_ == new_size)
298 // Growing notifications instantly can cause notification's to overlap
299 // And shrinking with animation, leaves area, where nothing is drawn
300 const bool change_instantly = preferred_size_.width() > new_size.width() ||
301 preferred_size_.height() > new_size.height();
302 preferred_size_ = new_size;
304 if (change_instantly) {
305 SetBoundsInstantly(bounds());
308 SetBoundsWithAnimation(bounds());
311 void ToastContentsView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
312 if (child_count() > 0)
313 child_at(0)->GetAccessibleNodeData(node_data);
314 node_data->role = ax::mojom::Role::kAlertDialog;
317 const char* ToastContentsView::GetClassName() const {
318 return kViewClassName;
321 void ToastContentsView::CreateWidget(
322 PopupAlignmentDelegate* alignment_delegate) {
323 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
324 params.keep_on_top = true;
325 #if (defined(OS_LINUX) && !defined(OS_CHROMEOS)) || defined(OS_BSD)
326 params.opacity = views::Widget::InitParams::OPAQUE_WINDOW;
328 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
330 params.delegate = this;
331 views::Widget* widget = new views::Widget();
332 alignment_delegate->ConfigureWidgetInitParamsForContainer(widget, ¶ms);
333 widget->set_focus_on_creation(false);
334 widget->AddObserver(this);
337 // We want to ensure that this toast always goes to the native desktop,
338 // not the Ash desktop (since there is already another toast contents view
341 params.native_widget = new views::DesktopNativeWidgetAura(widget);
344 widget->Init(params);
346 #if defined(OS_CHROMEOS)
347 // On Chrome OS, this widget is shown in the shelf container. It means this
348 // widget would inherit the parent's window targeter (ShelfWindowTarget) by
349 // default. But it is not good for popup. So we override it with the normal
351 gfx::NativeWindow native_window = widget->GetNativeWindow();
352 native_window->SetEventTargeter(std::make_unique<aura::WindowTargeter>());
356 gfx::Rect ToastContentsView::GetClosedToastBounds(gfx::Rect bounds) {
357 return gfx::Rect(bounds.x() + bounds.width() - kClosedToastWidth,
363 } // namespace message_center