• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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