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.
4
5 #include "ui/message_center/views/toast_contents_view.h"
6
7 #include "base/bind.h"
8 #include "base/compiler_specific.h"
9 #include "base/memory/scoped_ptr.h"
10 #include "base/memory/weak_ptr.h"
11 #include "base/time/time.h"
12 #include "base/timer/timer.h"
13 #include "ui/base/accessibility/accessible_view_state.h"
14 #include "ui/gfx/animation/animation_delegate.h"
15 #include "ui/gfx/animation/slide_animation.h"
16 #include "ui/gfx/display.h"
17 #include "ui/gfx/screen.h"
18 #include "ui/message_center/message_center_style.h"
19 #include "ui/message_center/notification.h"
20 #include "ui/message_center/views/message_popup_collection.h"
21 #include "ui/message_center/views/message_view.h"
22 #include "ui/views/background.h"
23 #include "ui/views/view.h"
24 #include "ui/views/widget/widget.h"
25 #include "ui/views/widget/widget_delegate.h"
26
27 #if defined(OS_WIN) && defined(USE_ASH)
28 #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
29 #endif
30
31 namespace message_center {
32 namespace {
33
34 // The width of a toast before animated reveal and after closing.
35 const int kClosedToastWidth = 5;
36
37 // FadeIn/Out look a bit better if they are slightly longer then default slide.
38 const int kFadeInOutDuration = 200;
39
40 } // namespace.
41
42 // static
GetToastSizeForView(views::View * view)43 gfx::Size ToastContentsView::GetToastSizeForView(views::View* view) {
44 int width = kNotificationWidth + view->GetInsets().width();
45 return gfx::Size(width, view->GetHeightForWidth(width));
46 }
47
ToastContentsView(const std::string & notification_id,base::WeakPtr<MessagePopupCollection> collection)48 ToastContentsView::ToastContentsView(
49 const std::string& notification_id,
50 base::WeakPtr<MessagePopupCollection> collection)
51 : collection_(collection),
52 id_(notification_id),
53 is_animating_bounds_(false),
54 is_closing_(false),
55 closing_animation_(NULL) {
56 set_notify_enter_exit_on_child(true);
57 // Sets the transparent background. Then, when the message view is slid out,
58 // the whole toast seems to slide although the actual bound of the widget
59 // remains. This is hacky but easier to keep the consistency.
60 set_background(views::Background::CreateSolidBackground(0, 0, 0, 0));
61
62 fade_animation_.reset(new gfx::SlideAnimation(this));
63 fade_animation_->SetSlideDuration(kFadeInOutDuration);
64
65 CreateWidget(collection->parent());
66 }
67
68 // This is destroyed when the toast window closes.
~ToastContentsView()69 ToastContentsView::~ToastContentsView() {
70 if (collection_)
71 collection_->ForgetToast(this);
72 }
73
SetContents(MessageView * view,bool a11y_feedback_for_updates)74 void ToastContentsView::SetContents(MessageView* view,
75 bool a11y_feedback_for_updates) {
76 bool already_has_contents = child_count() > 0;
77 RemoveAllChildViews(true);
78 AddChildView(view);
79 preferred_size_ = GetToastSizeForView(view);
80 Layout();
81
82 // If it has the contents already, this invocation means an update of the
83 // popup toast, and the new contents should be read through a11y feature.
84 // The notification type should be ALERT, otherwise the accessibility message
85 // won't be read for this view which returns ROLE_WINDOW.
86 if (already_has_contents && a11y_feedback_for_updates)
87 NotifyAccessibilityEvent(ui::AccessibilityTypes::EVENT_ALERT, false);
88 }
89
RevealWithAnimation(gfx::Point origin)90 void ToastContentsView::RevealWithAnimation(gfx::Point origin) {
91 // Place/move the toast widgets. Currently it stacks the widgets from the
92 // right-bottom of the work area.
93 // TODO(mukai): allow to specify the placement policy from outside of this
94 // class. The policy should be specified from preference on Windows, or
95 // the launcher alignment on ChromeOS.
96 origin_ = gfx::Point(origin.x() - preferred_size_.width(),
97 origin.y() - preferred_size_.height());
98
99 gfx::Rect stable_bounds(origin_, preferred_size_);
100
101 SetBoundsInstantly(GetClosedToastBounds(stable_bounds));
102 StartFadeIn();
103 SetBoundsWithAnimation(stable_bounds);
104 }
105
CloseWithAnimation()106 void ToastContentsView::CloseWithAnimation() {
107 if (is_closing_)
108 return;
109 is_closing_ = true;
110 StartFadeOut();
111 }
112
SetBoundsInstantly(gfx::Rect new_bounds)113 void ToastContentsView::SetBoundsInstantly(gfx::Rect new_bounds) {
114 if (new_bounds == bounds())
115 return;
116
117 origin_ = new_bounds.origin();
118 if (!GetWidget())
119 return;
120 GetWidget()->SetBounds(new_bounds);
121 }
122
SetBoundsWithAnimation(gfx::Rect new_bounds)123 void ToastContentsView::SetBoundsWithAnimation(gfx::Rect new_bounds) {
124 if (new_bounds == bounds())
125 return;
126
127 origin_ = new_bounds.origin();
128 if (!GetWidget())
129 return;
130
131 // This picks up the current bounds, so if there was a previous animation
132 // half-done, the next one will pick up from the current location.
133 // This is the only place that should query current location of the Widget
134 // on screen, the rest should refer to the bounds_.
135 animated_bounds_start_ = GetWidget()->GetWindowBoundsInScreen();
136 animated_bounds_end_ = new_bounds;
137
138 if (collection_)
139 collection_->IncrementDeferCounter();
140
141 if (bounds_animation_.get())
142 bounds_animation_->Stop();
143
144 bounds_animation_.reset(new gfx::SlideAnimation(this));
145 bounds_animation_->Show();
146 }
147
StartFadeIn()148 void ToastContentsView::StartFadeIn() {
149 // The decrement is done in OnBoundsAnimationEndedOrCancelled callback.
150 if (collection_)
151 collection_->IncrementDeferCounter();
152 fade_animation_->Stop();
153
154 GetWidget()->SetOpacity(0);
155 GetWidget()->Show();
156 fade_animation_->Reset(0);
157 fade_animation_->Show();
158 }
159
StartFadeOut()160 void ToastContentsView::StartFadeOut() {
161 // The decrement is done in OnBoundsAnimationEndedOrCancelled callback.
162 if (collection_)
163 collection_->IncrementDeferCounter();
164 fade_animation_->Stop();
165
166 closing_animation_ = (is_closing_ ? fade_animation_.get() : NULL);
167 fade_animation_->Reset(1);
168 fade_animation_->Hide();
169 }
170
OnBoundsAnimationEndedOrCancelled(const gfx::Animation * animation)171 void ToastContentsView::OnBoundsAnimationEndedOrCancelled(
172 const gfx::Animation* animation) {
173 if (is_closing_ && closing_animation_ == animation && GetWidget()) {
174 views::Widget* widget = GetWidget();
175 #if defined(USE_AURA)
176 // TODO(dewittj): This is a workaround to prevent a nasty bug where
177 // closing a transparent widget doesn't actually remove the window,
178 // causing entire areas of the screen to become unresponsive to clicks.
179 // See crbug.com/243469
180 widget->Hide();
181 # if defined(OS_WIN)
182 widget->SetOpacity(0xFF);
183 # endif
184 #endif
185 widget->Close();
186 }
187
188 // This cannot be called before GetWidget()->Close(). Decrementing defer count
189 // will invoke update, which may invoke another close animation with
190 // incrementing defer counter. Close() after such process will cause a
191 // mismatch between increment/decrement. See crbug.com/238477
192 if (collection_)
193 collection_->DecrementDeferCounter();
194 }
195
196 // gfx::AnimationDelegate
AnimationProgressed(const gfx::Animation * animation)197 void ToastContentsView::AnimationProgressed(const gfx::Animation* animation) {
198 if (animation == bounds_animation_.get()) {
199 gfx::Rect current(animation->CurrentValueBetween(
200 animated_bounds_start_, animated_bounds_end_));
201 GetWidget()->SetBounds(current);
202 } else if (animation == fade_animation_.get()) {
203 unsigned char opacity =
204 static_cast<unsigned char>(fade_animation_->GetCurrentValue() * 255);
205 GetWidget()->SetOpacity(opacity);
206 }
207 }
208
AnimationEnded(const gfx::Animation * animation)209 void ToastContentsView::AnimationEnded(const gfx::Animation* animation) {
210 OnBoundsAnimationEndedOrCancelled(animation);
211 }
212
AnimationCanceled(const gfx::Animation * animation)213 void ToastContentsView::AnimationCanceled(
214 const gfx::Animation* animation) {
215 OnBoundsAnimationEndedOrCancelled(animation);
216 }
217
218 // views::WidgetDelegate
GetContentsView()219 views::View* ToastContentsView::GetContentsView() {
220 return this;
221 }
222
WindowClosing()223 void ToastContentsView::WindowClosing() {
224 if (!is_closing_ && collection_.get())
225 collection_->ForgetToast(this);
226 }
227
CanActivate() const228 bool ToastContentsView::CanActivate() const {
229 #if defined(OS_WIN) && defined(USE_AURA)
230 return true;
231 #else
232 return false;
233 #endif
234 }
235
OnDisplayChanged()236 void ToastContentsView::OnDisplayChanged() {
237 views::Widget* widget = GetWidget();
238 if (!widget)
239 return;
240
241 gfx::NativeView native_view = widget->GetNativeView();
242 if (!native_view || !collection_.get())
243 return;
244
245 collection_->OnDisplayBoundsChanged(gfx::Screen::GetScreenFor(
246 native_view)->GetDisplayNearestWindow(native_view));
247 }
248
OnWorkAreaChanged()249 void ToastContentsView::OnWorkAreaChanged() {
250 views::Widget* widget = GetWidget();
251 if (!widget)
252 return;
253
254 gfx::NativeView native_view = widget->GetNativeView();
255 if (!native_view || !collection_.get())
256 return;
257
258 collection_->OnDisplayBoundsChanged(gfx::Screen::GetScreenFor(
259 native_view)->GetDisplayNearestWindow(native_view));
260 }
261
262 // views::View
OnMouseEntered(const ui::MouseEvent & event)263 void ToastContentsView::OnMouseEntered(const ui::MouseEvent& event) {
264 if (collection_)
265 collection_->OnMouseEntered(this);
266 }
267
OnMouseExited(const ui::MouseEvent & event)268 void ToastContentsView::OnMouseExited(const ui::MouseEvent& event) {
269 if (collection_)
270 collection_->OnMouseExited(this);
271 }
272
Layout()273 void ToastContentsView::Layout() {
274 if (child_count() > 0) {
275 child_at(0)->SetBounds(
276 0, 0, preferred_size_.width(), preferred_size_.height());
277 }
278 }
279
GetPreferredSize()280 gfx::Size ToastContentsView::GetPreferredSize() {
281 return child_count() ? GetToastSizeForView(child_at(0)) : gfx::Size();
282 }
283
GetAccessibleState(ui::AccessibleViewState * state)284 void ToastContentsView::GetAccessibleState(ui::AccessibleViewState* state) {
285 if (child_count() > 0)
286 child_at(0)->GetAccessibleState(state);
287 state->role = ui::AccessibilityTypes::ROLE_WINDOW;
288 }
289
ClickOnNotification(const std::string & notification_id)290 void ToastContentsView::ClickOnNotification(
291 const std::string& notification_id) {
292 if (collection_)
293 collection_->ClickOnNotification(notification_id);
294 }
295
RemoveNotification(const std::string & notification_id,bool by_user)296 void ToastContentsView::RemoveNotification(
297 const std::string& notification_id,
298 bool by_user) {
299 if (collection_)
300 collection_->RemoveNotification(notification_id, by_user);
301 }
302
DisableNotificationsFromThisSource(const NotifierId & notifier_id)303 void ToastContentsView::DisableNotificationsFromThisSource(
304 const NotifierId& notifier_id) {
305 if (collection_)
306 collection_->DisableNotificationsFromThisSource(notifier_id);
307 }
308
ShowNotifierSettingsBubble()309 void ToastContentsView::ShowNotifierSettingsBubble() {
310 if (collection_)
311 collection_->ShowNotifierSettingsBubble();
312 }
313
HasClickedListener(const std::string & notification_id)314 bool ToastContentsView::HasClickedListener(
315 const std::string& notification_id) {
316 if (!collection_)
317 return false;
318 return collection_->HasClickedListener(notification_id);
319 }
320
ClickOnNotificationButton(const std::string & notification_id,int button_index)321 void ToastContentsView::ClickOnNotificationButton(
322 const std::string& notification_id,
323 int button_index) {
324 if (collection_)
325 collection_->ClickOnNotificationButton(notification_id, button_index);
326 }
327
ExpandNotification(const std::string & notification_id)328 void ToastContentsView::ExpandNotification(
329 const std::string& notification_id) {
330 if (collection_)
331 collection_->ExpandNotification(notification_id);
332 }
333
GroupBodyClicked(const std::string & last_notification_id)334 void ToastContentsView::GroupBodyClicked(
335 const std::string& last_notification_id) {
336 // No group views in popup collection.
337 NOTREACHED();
338 }
339
340 // When clicked on the "N more" button, perform some reasonable action.
341 // TODO(dimich): find out what the reasonable action could be.
ExpandGroup(const NotifierId & notifier_id)342 void ToastContentsView::ExpandGroup(const NotifierId& notifier_id) {
343 // No group views in popup collection.
344 NOTREACHED();
345 }
346
RemoveGroup(const NotifierId & notifier_id)347 void ToastContentsView::RemoveGroup(const NotifierId& notifier_id) {
348 // No group views in popup collection.
349 NOTREACHED();
350 }
351
CreateWidget(gfx::NativeView parent)352 void ToastContentsView::CreateWidget(gfx::NativeView parent) {
353 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
354 params.keep_on_top = true;
355 if (parent)
356 params.parent = parent;
357 else
358 params.top_level = true;
359 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
360 params.delegate = this;
361 views::Widget* widget = new views::Widget();
362 widget->set_focus_on_creation(false);
363
364 #if defined(OS_WIN) && defined(USE_ASH)
365 // We want to ensure that this toast always goes to the native desktop,
366 // not the Ash desktop (since there is already another toast contents view
367 // there.
368 if (!params.parent)
369 params.native_widget = new views::DesktopNativeWidgetAura(widget);
370 #endif
371
372 widget->Init(params);
373 }
374
GetClosedToastBounds(gfx::Rect bounds)375 gfx::Rect ToastContentsView::GetClosedToastBounds(gfx::Rect bounds) {
376 return gfx::Rect(bounds.x() + bounds.width() - kClosedToastWidth,
377 bounds.y(),
378 kClosedToastWidth,
379 bounds.height());
380 }
381
382 } // namespace message_center
383