• 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/message_popup_collection.h"
6 
7 #include <set>
8 
9 #include "base/bind.h"
10 #include "base/i18n/rtl.h"
11 #include "base/logging.h"
12 #include "base/memory/weak_ptr.h"
13 #include "base/run_loop.h"
14 #include "base/time/time.h"
15 #include "base/timer/timer.h"
16 #include "ui/accessibility/ax_enums.h"
17 #include "ui/gfx/animation/animation_delegate.h"
18 #include "ui/gfx/animation/slide_animation.h"
19 #include "ui/gfx/screen.h"
20 #include "ui/message_center/message_center.h"
21 #include "ui/message_center/message_center_style.h"
22 #include "ui/message_center/message_center_tray.h"
23 #include "ui/message_center/notification.h"
24 #include "ui/message_center/notification_list.h"
25 #include "ui/message_center/views/message_view_context_menu_controller.h"
26 #include "ui/message_center/views/notification_view.h"
27 #include "ui/message_center/views/popup_alignment_delegate.h"
28 #include "ui/message_center/views/toast_contents_view.h"
29 #include "ui/views/background.h"
30 #include "ui/views/layout/fill_layout.h"
31 #include "ui/views/view.h"
32 #include "ui/views/views_delegate.h"
33 #include "ui/views/widget/widget.h"
34 #include "ui/views/widget/widget_delegate.h"
35 
36 namespace message_center {
37 namespace {
38 
39 // Timeout between the last user-initiated close of the toast and the moment
40 // when normal layout/update of the toast stack continues. If the last toast was
41 // just closed, the timeout is shorter.
42 const int kMouseExitedDeferTimeoutMs = 200;
43 
44 // The margin between messages (and between the anchor unless
45 // first_item_has_no_margin was specified).
46 const int kToastMarginY = kMarginBetweenItems;
47 
48 }  // namespace.
49 
MessagePopupCollection(gfx::NativeView parent,MessageCenter * message_center,MessageCenterTray * tray,PopupAlignmentDelegate * alignment_delegate)50 MessagePopupCollection::MessagePopupCollection(
51     gfx::NativeView parent,
52     MessageCenter* message_center,
53     MessageCenterTray* tray,
54     PopupAlignmentDelegate* alignment_delegate)
55     : parent_(parent),
56       message_center_(message_center),
57       tray_(tray),
58       alignment_delegate_(alignment_delegate),
59       defer_counter_(0),
60       latest_toast_entered_(NULL),
61       user_is_closing_toasts_by_clicking_(false),
62       context_menu_controller_(new MessageViewContextMenuController(this)),
63       weak_factory_(this) {
64   DCHECK(message_center_);
65   defer_timer_.reset(new base::OneShotTimer<MessagePopupCollection>);
66   message_center_->AddObserver(this);
67   alignment_delegate_->set_collection(this);
68 }
69 
~MessagePopupCollection()70 MessagePopupCollection::~MessagePopupCollection() {
71   weak_factory_.InvalidateWeakPtrs();
72 
73   message_center_->RemoveObserver(this);
74 
75   CloseAllWidgets();
76 }
77 
ClickOnNotification(const std::string & notification_id)78 void MessagePopupCollection::ClickOnNotification(
79     const std::string& notification_id) {
80   message_center_->ClickOnNotification(notification_id);
81 }
82 
RemoveNotification(const std::string & notification_id,bool by_user)83 void MessagePopupCollection::RemoveNotification(
84     const std::string& notification_id,
85     bool by_user) {
86   message_center_->RemoveNotification(notification_id, by_user);
87 }
88 
CreateMenuModel(const NotifierId & notifier_id,const base::string16 & display_source)89 scoped_ptr<ui::MenuModel> MessagePopupCollection::CreateMenuModel(
90     const NotifierId& notifier_id,
91     const base::string16& display_source) {
92   return tray_->CreateNotificationMenuModel(notifier_id, display_source);
93 }
94 
HasClickedListener(const std::string & notification_id)95 bool MessagePopupCollection::HasClickedListener(
96     const std::string& notification_id) {
97   return message_center_->HasClickedListener(notification_id);
98 }
99 
ClickOnNotificationButton(const std::string & notification_id,int button_index)100 void MessagePopupCollection::ClickOnNotificationButton(
101     const std::string& notification_id,
102     int button_index) {
103   message_center_->ClickOnNotificationButton(notification_id, button_index);
104 }
105 
MarkAllPopupsShown()106 void MessagePopupCollection::MarkAllPopupsShown() {
107   std::set<std::string> closed_ids = CloseAllWidgets();
108   for (std::set<std::string>::iterator iter = closed_ids.begin();
109        iter != closed_ids.end(); iter++) {
110     message_center_->MarkSinglePopupAsShown(*iter, false);
111   }
112 }
113 
UpdateWidgets()114 void MessagePopupCollection::UpdateWidgets() {
115   NotificationList::PopupNotifications popups =
116       message_center_->GetPopupNotifications();
117 
118   if (popups.empty()) {
119     CloseAllWidgets();
120     return;
121   }
122 
123   bool top_down = alignment_delegate_->IsTopDown();
124   int base = GetBaseLine(toasts_.empty() ? NULL : toasts_.back());
125 
126   // Iterate in the reverse order to keep the oldest toasts on screen. Newer
127   // items may be ignored if there are no room to place them.
128   for (NotificationList::PopupNotifications::const_reverse_iterator iter =
129            popups.rbegin(); iter != popups.rend(); ++iter) {
130     if (FindToast((*iter)->id()))
131       continue;
132 
133     NotificationView* view =
134         NotificationView::Create(NULL,
135                                  *(*iter),
136                                  true); // Create top-level notification.
137     view->set_context_menu_controller(context_menu_controller_.get());
138     int view_height = ToastContentsView::GetToastSizeForView(view).height();
139     int height_available =
140         top_down ? alignment_delegate_->GetWorkAreaBottom() - base : base;
141 
142     if (height_available - view_height - kToastMarginY < 0) {
143       delete view;
144       break;
145     }
146 
147     ToastContentsView* toast =
148         new ToastContentsView((*iter)->id(), weak_factory_.GetWeakPtr());
149     // There will be no contents already since this is a new ToastContentsView.
150     toast->SetContents(view, /*a11y_feedback_for_updates=*/false);
151     toasts_.push_back(toast);
152     view->set_controller(toast);
153 
154     gfx::Size preferred_size = toast->GetPreferredSize();
155     gfx::Point origin(
156         alignment_delegate_->GetToastOriginX(gfx::Rect(preferred_size)), base);
157     // The toast slides in from the edge of the screen horizontally.
158     if (alignment_delegate_->IsFromLeft())
159       origin.set_x(origin.x() - preferred_size.width());
160     else
161       origin.set_x(origin.x() + preferred_size.width());
162     if (top_down)
163       origin.set_y(origin.y() + view_height);
164 
165     toast->RevealWithAnimation(origin);
166 
167     // Shift the base line to be a few pixels above the last added toast or (few
168     // pixels below last added toast if top-aligned).
169     if (top_down)
170       base += view_height + kToastMarginY;
171     else
172       base -= view_height + kToastMarginY;
173 
174     if (views::ViewsDelegate::views_delegate) {
175       views::ViewsDelegate::views_delegate->NotifyAccessibilityEvent(
176           toast, ui::AX_EVENT_ALERT);
177     }
178 
179     message_center_->DisplayedNotification(
180         (*iter)->id(), message_center::DISPLAY_SOURCE_POPUP);
181   }
182 }
183 
OnMouseEntered(ToastContentsView * toast_entered)184 void MessagePopupCollection::OnMouseEntered(ToastContentsView* toast_entered) {
185   // Sometimes we can get two MouseEntered/MouseExited in a row when animating
186   // toasts.  So we need to keep track of which one is the currently active one.
187   latest_toast_entered_ = toast_entered;
188 
189   message_center_->PausePopupTimers();
190 
191   if (user_is_closing_toasts_by_clicking_)
192     defer_timer_->Stop();
193 }
194 
OnMouseExited(ToastContentsView * toast_exited)195 void MessagePopupCollection::OnMouseExited(ToastContentsView* toast_exited) {
196   // If we're exiting a toast after entering a different toast, then ignore
197   // this mouse event.
198   if (toast_exited != latest_toast_entered_)
199     return;
200   latest_toast_entered_ = NULL;
201 
202   if (user_is_closing_toasts_by_clicking_) {
203     defer_timer_->Start(
204         FROM_HERE,
205         base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs),
206         this,
207         &MessagePopupCollection::OnDeferTimerExpired);
208   } else {
209     message_center_->RestartPopupTimers();
210   }
211 }
212 
CloseAllWidgets()213 std::set<std::string> MessagePopupCollection::CloseAllWidgets() {
214   std::set<std::string> closed_toast_ids;
215 
216   while (!toasts_.empty()) {
217     ToastContentsView* toast = toasts_.front();
218     toasts_.pop_front();
219     closed_toast_ids.insert(toast->id());
220 
221     OnMouseExited(toast);
222 
223     // CloseWithAnimation will cause the toast to forget about |this| so it is
224     // required when we forget a toast.
225     toast->CloseWithAnimation();
226   }
227 
228   return closed_toast_ids;
229 }
230 
ForgetToast(ToastContentsView * toast)231 void MessagePopupCollection::ForgetToast(ToastContentsView* toast) {
232   toasts_.remove(toast);
233   OnMouseExited(toast);
234 }
235 
RemoveToast(ToastContentsView * toast,bool mark_as_shown)236 void MessagePopupCollection::RemoveToast(ToastContentsView* toast,
237                                          bool mark_as_shown) {
238   ForgetToast(toast);
239 
240   toast->CloseWithAnimation();
241 
242   if (mark_as_shown)
243     message_center_->MarkSinglePopupAsShown(toast->id(), false);
244 }
245 
RepositionWidgets()246 void MessagePopupCollection::RepositionWidgets() {
247   bool top_down = alignment_delegate_->IsTopDown();
248   int base = GetBaseLine(NULL);  // We don't want to position relative to last
249                                  // toast - we want re-position.
250 
251   for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();) {
252     Toasts::const_iterator curr = iter++;
253     gfx::Rect bounds((*curr)->bounds());
254     bounds.set_x(alignment_delegate_->GetToastOriginX(bounds));
255     bounds.set_y(top_down ? base : base - bounds.height());
256 
257     // The notification may scrolls the boundary of the screen due to image
258     // load and such notifications should disappear. Do not call
259     // CloseWithAnimation, we don't want to show the closing animation, and we
260     // don't want to mark such notifications as shown. See crbug.com/233424
261     if ((top_down ? alignment_delegate_->GetWorkAreaBottom() - bounds.bottom()
262                   : bounds.y()) >= 0)
263       (*curr)->SetBoundsWithAnimation(bounds);
264     else
265       RemoveToast(*curr, /*mark_as_shown=*/false);
266 
267     // Shift the base line to be a few pixels above the last added toast or (few
268     // pixels below last added toast if top-aligned).
269     if (top_down)
270       base += bounds.height() + kToastMarginY;
271     else
272       base -= bounds.height() + kToastMarginY;
273   }
274 }
275 
RepositionWidgetsWithTarget()276 void MessagePopupCollection::RepositionWidgetsWithTarget() {
277   if (toasts_.empty())
278     return;
279 
280   bool top_down = alignment_delegate_->IsTopDown();
281 
282   // Nothing to do if there are no widgets above target if bottom-aligned or no
283   // widgets below target if top-aligned.
284   if (top_down ? toasts_.back()->origin().y() < target_top_edge_
285                : toasts_.back()->origin().y() > target_top_edge_)
286     return;
287 
288   Toasts::reverse_iterator iter = toasts_.rbegin();
289   for (; iter != toasts_.rend(); ++iter) {
290     // We only reposition widgets above target if bottom-aligned or widgets
291     // below target if top-aligned.
292     if (top_down ? (*iter)->origin().y() < target_top_edge_
293                  : (*iter)->origin().y() > target_top_edge_)
294       break;
295   }
296   --iter;
297 
298   // Slide length is the number of pixels the widgets should move so that their
299   // bottom edge (top-edge if top-aligned) touches the target.
300   int slide_length = std::abs(target_top_edge_ - (*iter)->origin().y());
301   for (;; --iter) {
302     gfx::Rect bounds((*iter)->bounds());
303 
304     // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
305     // shift them downwards by slide_length.
306     if (top_down)
307       bounds.set_y(bounds.y() - slide_length);
308     else
309       bounds.set_y(bounds.y() + slide_length);
310     (*iter)->SetBoundsWithAnimation(bounds);
311 
312     if (iter == toasts_.rbegin())
313       break;
314   }
315 }
316 
GetBaseLine(ToastContentsView * last_toast) const317 int MessagePopupCollection::GetBaseLine(ToastContentsView* last_toast) const {
318   if (!last_toast) {
319     return alignment_delegate_->GetBaseLine();
320   } else if (alignment_delegate_->IsTopDown()) {
321     return toasts_.back()->bounds().bottom() + kToastMarginY;
322   } else {
323     return toasts_.back()->origin().y() - kToastMarginY;
324   }
325 }
326 
OnNotificationAdded(const std::string & notification_id)327 void MessagePopupCollection::OnNotificationAdded(
328     const std::string& notification_id) {
329   DoUpdateIfPossible();
330 }
331 
OnNotificationRemoved(const std::string & notification_id,bool by_user)332 void MessagePopupCollection::OnNotificationRemoved(
333     const std::string& notification_id,
334     bool by_user) {
335   // Find a toast.
336   Toasts::const_iterator iter = toasts_.begin();
337   for (; iter != toasts_.end(); ++iter) {
338     if ((*iter)->id() == notification_id)
339       break;
340   }
341   if (iter == toasts_.end())
342     return;
343 
344   target_top_edge_ = (*iter)->bounds().y();
345   if (by_user && !user_is_closing_toasts_by_clicking_) {
346     // [Re] start a timeout after which the toasts re-position to their
347     // normal locations after tracking the mouse pointer for easy deletion.
348     // This provides a period of time when toasts are easy to remove because
349     // they re-position themselves to have Close button right under the mouse
350     // pointer. If the user continue to remove the toasts, the delay is reset.
351     // Once user stopped removing the toasts, the toasts re-populate/rearrange
352     // after the specified delay.
353     user_is_closing_toasts_by_clicking_ = true;
354     IncrementDeferCounter();
355   }
356 
357   // CloseWithAnimation ultimately causes a call to RemoveToast, which calls
358   // OnMouseExited.  This means that |user_is_closing_toasts_by_clicking_| must
359   // have been set before this call, otherwise it will remain true even after
360   // the toast is closed, since the defer timer won't be started.
361   RemoveToast(*iter, /*mark_as_shown=*/true);
362 
363   if (by_user)
364     RepositionWidgetsWithTarget();
365 }
366 
OnDeferTimerExpired()367 void MessagePopupCollection::OnDeferTimerExpired() {
368   user_is_closing_toasts_by_clicking_ = false;
369   DecrementDeferCounter();
370 
371   message_center_->RestartPopupTimers();
372 }
373 
OnNotificationUpdated(const std::string & notification_id)374 void MessagePopupCollection::OnNotificationUpdated(
375     const std::string& notification_id) {
376   // Find a toast.
377   Toasts::const_iterator toast_iter = toasts_.begin();
378   for (; toast_iter != toasts_.end(); ++toast_iter) {
379     if ((*toast_iter)->id() == notification_id)
380       break;
381   }
382   if (toast_iter == toasts_.end())
383     return;
384 
385   NotificationList::PopupNotifications notifications =
386       message_center_->GetPopupNotifications();
387   bool updated = false;
388 
389   for (NotificationList::PopupNotifications::iterator iter =
390            notifications.begin(); iter != notifications.end(); ++iter) {
391     Notification* notification = *iter;
392     DCHECK(notification);
393     ToastContentsView* toast_contents_view = *toast_iter;
394     DCHECK(toast_contents_view);
395 
396     if (notification->id() != notification_id)
397       continue;
398 
399     const RichNotificationData& optional_fields =
400         notification->rich_notification_data();
401     bool a11y_feedback_for_updates =
402         optional_fields.should_make_spoken_feedback_for_popup_updates;
403 
404     toast_contents_view->UpdateContents(*notification,
405                                         a11y_feedback_for_updates);
406 
407     updated = true;
408   }
409 
410   // OnNotificationUpdated() can be called when a notification is excluded from
411   // the popup notification list but still remains in the full notification
412   // list. In that case the widget for the notification has to be closed here.
413   if (!updated)
414     RemoveToast(*toast_iter, /*mark_as_shown=*/true);
415 
416   if (user_is_closing_toasts_by_clicking_)
417     RepositionWidgetsWithTarget();
418   else
419     DoUpdateIfPossible();
420 }
421 
FindToast(const std::string & notification_id) const422 ToastContentsView* MessagePopupCollection::FindToast(
423     const std::string& notification_id) const {
424   for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
425        ++iter) {
426     if ((*iter)->id() == notification_id)
427       return *iter;
428   }
429   return NULL;
430 }
431 
IncrementDeferCounter()432 void MessagePopupCollection::IncrementDeferCounter() {
433   defer_counter_++;
434 }
435 
DecrementDeferCounter()436 void MessagePopupCollection::DecrementDeferCounter() {
437   defer_counter_--;
438   DCHECK(defer_counter_ >= 0);
439   DoUpdateIfPossible();
440 }
441 
442 // This is the main sequencer of tasks. It does a step, then waits for
443 // all started transitions to play out before doing the next step.
444 // First, remove all expired toasts.
445 // Then, reposition widgets (the reposition on close happens before all
446 // deferred tasks are even able to run)
447 // Then, see if there is vacant space for new toasts.
DoUpdateIfPossible()448 void MessagePopupCollection::DoUpdateIfPossible() {
449   if (defer_counter_ > 0)
450     return;
451 
452   RepositionWidgets();
453 
454   if (defer_counter_ > 0)
455     return;
456 
457   // Reposition could create extra space which allows additional widgets.
458   UpdateWidgets();
459 
460   if (defer_counter_ > 0)
461     return;
462 
463   // Test support. Quit the test run loop when no more updates are deferred,
464   // meaining th echeck for updates did not cause anything to change so no new
465   // transition animations were started.
466   if (run_loop_for_test_.get())
467     run_loop_for_test_->Quit();
468 }
469 
OnDisplayMetricsChanged(const gfx::Display & display)470 void MessagePopupCollection::OnDisplayMetricsChanged(
471     const gfx::Display& display) {
472   alignment_delegate_->RecomputeAlignment(display);
473 }
474 
GetWidgetForTest(const std::string & id) const475 views::Widget* MessagePopupCollection::GetWidgetForTest(const std::string& id)
476     const {
477   for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
478        ++iter) {
479     if ((*iter)->id() == id)
480       return (*iter)->GetWidget();
481   }
482   return NULL;
483 }
484 
CreateRunLoopForTest()485 void MessagePopupCollection::CreateRunLoopForTest() {
486   run_loop_for_test_.reset(new base::RunLoop());
487 }
488 
WaitForTest()489 void MessagePopupCollection::WaitForTest() {
490   run_loop_for_test_->Run();
491   run_loop_for_test_.reset();
492 }
493 
GetToastRectAt(size_t index) const494 gfx::Rect MessagePopupCollection::GetToastRectAt(size_t index) const {
495   DCHECK(defer_counter_ == 0) << "Fetching the bounds with animations active.";
496   size_t i = 0;
497   for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
498        ++iter) {
499     if (i++ == index) {
500       views::Widget* widget = (*iter)->GetWidget();
501       if (widget)
502         return widget->GetWindowBoundsInScreen();
503       break;
504     }
505   }
506   return gfx::Rect();
507 }
508 
509 }  // namespace message_center
510