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