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/message_popup_collection.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"
36 namespace message_center
{
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;
44 // The margin between messages (and between the anchor unless
45 // first_item_has_no_margin was specified).
46 const int kToastMarginY
= kMarginBetweenItems
;
50 MessagePopupCollection::MessagePopupCollection(
51 gfx::NativeView parent
,
52 MessageCenter
* message_center
,
53 MessageCenterTray
* tray
,
54 PopupAlignmentDelegate
* alignment_delegate
)
56 message_center_(message_center
),
58 alignment_delegate_(alignment_delegate
),
60 latest_toast_entered_(NULL
),
61 user_is_closing_toasts_by_clicking_(false),
62 context_menu_controller_(new MessageViewContextMenuController(this)),
64 DCHECK(message_center_
);
65 defer_timer_
.reset(new base::OneShotTimer
<MessagePopupCollection
>);
66 message_center_
->AddObserver(this);
67 alignment_delegate_
->set_collection(this);
70 MessagePopupCollection::~MessagePopupCollection() {
71 weak_factory_
.InvalidateWeakPtrs();
73 message_center_
->RemoveObserver(this);
78 void MessagePopupCollection::ClickOnNotification(
79 const std::string
& notification_id
) {
80 message_center_
->ClickOnNotification(notification_id
);
83 void MessagePopupCollection::RemoveNotification(
84 const std::string
& notification_id
,
86 message_center_
->RemoveNotification(notification_id
, by_user
);
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
);
95 bool MessagePopupCollection::HasClickedListener(
96 const std::string
& notification_id
) {
97 return message_center_
->HasClickedListener(notification_id
);
100 void MessagePopupCollection::ClickOnNotificationButton(
101 const std::string
& notification_id
,
103 message_center_
->ClickOnNotificationButton(notification_id
, button_index
);
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);
114 void MessagePopupCollection::UpdateWidgets() {
115 if (message_center_
->IsMessageCenterVisible()) {
116 DCHECK_EQ(0u, message_center_
->GetPopupNotifications().size());
120 NotificationList::PopupNotifications popups
=
121 message_center_
->GetPopupNotifications();
122 if (popups
.empty()) {
127 bool top_down
= alignment_delegate_
->IsTopDown();
128 int base
= GetBaseLine(toasts_
.empty() ? NULL
: toasts_
.back());
130 // Iterate in the reverse order to keep the oldest toasts on screen. Newer
131 // items may be ignored if there are no room to place them.
132 for (NotificationList::PopupNotifications::const_reverse_iterator iter
=
133 popups
.rbegin(); iter
!= popups
.rend(); ++iter
) {
134 if (FindToast((*iter
)->id()))
137 NotificationView
* view
=
138 NotificationView::Create(NULL
,
140 true); // Create top-level notification.
141 view
->set_context_menu_controller(context_menu_controller_
.get());
142 int view_height
= ToastContentsView::GetToastSizeForView(view
).height();
143 int height_available
=
144 top_down
? alignment_delegate_
->GetWorkAreaBottom() - base
: base
;
146 if (height_available
- view_height
- kToastMarginY
< 0) {
151 ToastContentsView
* toast
=
152 new ToastContentsView((*iter
)->id(), weak_factory_
.GetWeakPtr());
153 // There will be no contents already since this is a new ToastContentsView.
154 toast
->SetContents(view
, /*a11y_feedback_for_updates=*/false);
155 toasts_
.push_back(toast
);
156 view
->set_controller(toast
);
158 gfx::Size preferred_size
= toast
->GetPreferredSize();
160 alignment_delegate_
->GetToastOriginX(gfx::Rect(preferred_size
)), base
);
161 // The toast slides in from the edge of the screen horizontally.
162 if (alignment_delegate_
->IsFromLeft())
163 origin
.set_x(origin
.x() - preferred_size
.width());
165 origin
.set_x(origin
.x() + preferred_size
.width());
167 origin
.set_y(origin
.y() + view_height
);
169 toast
->RevealWithAnimation(origin
);
171 // Shift the base line to be a few pixels above the last added toast or (few
172 // pixels below last added toast if top-aligned).
174 base
+= view_height
+ kToastMarginY
;
176 base
-= view_height
+ kToastMarginY
;
178 if (views::ViewsDelegate::GetInstance()) {
179 views::ViewsDelegate::GetInstance()->NotifyAccessibilityEvent(
180 toast
, ui::AX_EVENT_ALERT
);
183 message_center_
->DisplayedNotification(
184 (*iter
)->id(), message_center::DISPLAY_SOURCE_POPUP
);
188 void MessagePopupCollection::OnMouseEntered(ToastContentsView
* toast_entered
) {
189 // Sometimes we can get two MouseEntered/MouseExited in a row when animating
190 // toasts. So we need to keep track of which one is the currently active one.
191 latest_toast_entered_
= toast_entered
;
193 message_center_
->PausePopupTimers();
195 if (user_is_closing_toasts_by_clicking_
)
196 defer_timer_
->Stop();
199 void MessagePopupCollection::OnMouseExited(ToastContentsView
* toast_exited
) {
200 // If we're exiting a toast after entering a different toast, then ignore
202 if (toast_exited
!= latest_toast_entered_
)
204 latest_toast_entered_
= NULL
;
206 if (user_is_closing_toasts_by_clicking_
) {
209 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs
),
211 &MessagePopupCollection::OnDeferTimerExpired
);
213 message_center_
->RestartPopupTimers();
217 std::set
<std::string
> MessagePopupCollection::CloseAllWidgets() {
218 std::set
<std::string
> closed_toast_ids
;
220 while (!toasts_
.empty()) {
221 ToastContentsView
* toast
= toasts_
.front();
223 closed_toast_ids
.insert(toast
->id());
225 OnMouseExited(toast
);
227 // CloseWithAnimation will cause the toast to forget about |this| so it is
228 // required when we forget a toast.
229 toast
->CloseWithAnimation();
232 return closed_toast_ids
;
235 void MessagePopupCollection::ForgetToast(ToastContentsView
* toast
) {
236 toasts_
.remove(toast
);
237 OnMouseExited(toast
);
240 void MessagePopupCollection::RemoveToast(ToastContentsView
* toast
,
241 bool mark_as_shown
) {
244 toast
->CloseWithAnimation();
247 message_center_
->MarkSinglePopupAsShown(toast
->id(), false);
250 void MessagePopupCollection::RepositionWidgets() {
251 bool top_down
= alignment_delegate_
->IsTopDown();
252 int base
= GetBaseLine(NULL
); // We don't want to position relative to last
253 // toast - we want re-position.
255 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();) {
256 Toasts::const_iterator curr
= iter
++;
257 gfx::Rect
bounds((*curr
)->bounds());
258 bounds
.set_x(alignment_delegate_
->GetToastOriginX(bounds
));
259 bounds
.set_y(top_down
? base
: base
- bounds
.height());
261 // The notification may scrolls the boundary of the screen due to image
262 // load and such notifications should disappear. Do not call
263 // CloseWithAnimation, we don't want to show the closing animation, and we
264 // don't want to mark such notifications as shown. See crbug.com/233424
265 if ((top_down
? alignment_delegate_
->GetWorkAreaBottom() - bounds
.bottom()
267 (*curr
)->SetBoundsWithAnimation(bounds
);
269 RemoveToast(*curr
, /*mark_as_shown=*/false);
271 // Shift the base line to be a few pixels above the last added toast or (few
272 // pixels below last added toast if top-aligned).
274 base
+= bounds
.height() + kToastMarginY
;
276 base
-= bounds
.height() + kToastMarginY
;
280 void MessagePopupCollection::RepositionWidgetsWithTarget() {
284 bool top_down
= alignment_delegate_
->IsTopDown();
286 // Nothing to do if there are no widgets above target if bottom-aligned or no
287 // widgets below target if top-aligned.
288 if (top_down
? toasts_
.back()->origin().y() < target_top_edge_
289 : toasts_
.back()->origin().y() > target_top_edge_
)
292 Toasts::reverse_iterator iter
= toasts_
.rbegin();
293 for (; iter
!= toasts_
.rend(); ++iter
) {
294 // We only reposition widgets above target if bottom-aligned or widgets
295 // below target if top-aligned.
296 if (top_down
? (*iter
)->origin().y() < target_top_edge_
297 : (*iter
)->origin().y() > target_top_edge_
)
302 // Slide length is the number of pixels the widgets should move so that their
303 // bottom edge (top-edge if top-aligned) touches the target.
304 int slide_length
= std::abs(target_top_edge_
- (*iter
)->origin().y());
306 gfx::Rect
bounds((*iter
)->bounds());
308 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
309 // shift them downwards by slide_length.
311 bounds
.set_y(bounds
.y() - slide_length
);
313 bounds
.set_y(bounds
.y() + slide_length
);
314 (*iter
)->SetBoundsWithAnimation(bounds
);
316 if (iter
== toasts_
.rbegin())
321 int MessagePopupCollection::GetBaseLine(ToastContentsView
* last_toast
) const {
323 return alignment_delegate_
->GetBaseLine();
324 } else if (alignment_delegate_
->IsTopDown()) {
325 return toasts_
.back()->bounds().bottom() + kToastMarginY
;
327 return toasts_
.back()->origin().y() - kToastMarginY
;
331 void MessagePopupCollection::OnNotificationAdded(
332 const std::string
& notification_id
) {
333 DoUpdateIfPossible();
336 void MessagePopupCollection::OnNotificationRemoved(
337 const std::string
& notification_id
,
340 Toasts::const_iterator iter
= toasts_
.begin();
341 for (; iter
!= toasts_
.end(); ++iter
) {
342 if ((*iter
)->id() == notification_id
)
345 if (iter
== toasts_
.end())
348 target_top_edge_
= (*iter
)->bounds().y();
349 if (by_user
&& !user_is_closing_toasts_by_clicking_
) {
350 // [Re] start a timeout after which the toasts re-position to their
351 // normal locations after tracking the mouse pointer for easy deletion.
352 // This provides a period of time when toasts are easy to remove because
353 // they re-position themselves to have Close button right under the mouse
354 // pointer. If the user continue to remove the toasts, the delay is reset.
355 // Once user stopped removing the toasts, the toasts re-populate/rearrange
356 // after the specified delay.
357 user_is_closing_toasts_by_clicking_
= true;
358 IncrementDeferCounter();
361 // CloseWithAnimation ultimately causes a call to RemoveToast, which calls
362 // OnMouseExited. This means that |user_is_closing_toasts_by_clicking_| must
363 // have been set before this call, otherwise it will remain true even after
364 // the toast is closed, since the defer timer won't be started.
365 RemoveToast(*iter
, /*mark_as_shown=*/true);
368 RepositionWidgetsWithTarget();
371 void MessagePopupCollection::OnDeferTimerExpired() {
372 user_is_closing_toasts_by_clicking_
= false;
373 DecrementDeferCounter();
375 message_center_
->RestartPopupTimers();
378 void MessagePopupCollection::OnNotificationUpdated(
379 const std::string
& notification_id
) {
381 Toasts::const_iterator toast_iter
= toasts_
.begin();
382 for (; toast_iter
!= toasts_
.end(); ++toast_iter
) {
383 if ((*toast_iter
)->id() == notification_id
)
386 if (toast_iter
== toasts_
.end())
389 NotificationList::PopupNotifications notifications
=
390 message_center_
->GetPopupNotifications();
391 bool updated
= false;
393 for (NotificationList::PopupNotifications::iterator iter
=
394 notifications
.begin(); iter
!= notifications
.end(); ++iter
) {
395 Notification
* notification
= *iter
;
396 DCHECK(notification
);
397 ToastContentsView
* toast_contents_view
= *toast_iter
;
398 DCHECK(toast_contents_view
);
400 if (notification
->id() != notification_id
)
403 const RichNotificationData
& optional_fields
=
404 notification
->rich_notification_data();
405 bool a11y_feedback_for_updates
=
406 optional_fields
.should_make_spoken_feedback_for_popup_updates
;
408 toast_contents_view
->UpdateContents(*notification
,
409 a11y_feedback_for_updates
);
414 // OnNotificationUpdated() can be called when a notification is excluded from
415 // the popup notification list but still remains in the full notification
416 // list. In that case the widget for the notification has to be closed here.
418 RemoveToast(*toast_iter
, /*mark_as_shown=*/true);
420 if (user_is_closing_toasts_by_clicking_
)
421 RepositionWidgetsWithTarget();
423 DoUpdateIfPossible();
426 ToastContentsView
* MessagePopupCollection::FindToast(
427 const std::string
& notification_id
) const {
428 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
430 if ((*iter
)->id() == notification_id
)
436 void MessagePopupCollection::IncrementDeferCounter() {
440 void MessagePopupCollection::DecrementDeferCounter() {
442 DCHECK(defer_counter_
>= 0);
443 DoUpdateIfPossible();
446 // This is the main sequencer of tasks. It does a step, then waits for
447 // all started transitions to play out before doing the next step.
448 // First, remove all expired toasts.
449 // Then, reposition widgets (the reposition on close happens before all
450 // deferred tasks are even able to run)
451 // Then, see if there is vacant space for new toasts.
452 void MessagePopupCollection::DoUpdateIfPossible() {
453 if (defer_counter_
> 0)
458 if (defer_counter_
> 0)
461 // Reposition could create extra space which allows additional widgets.
464 if (defer_counter_
> 0)
467 // Test support. Quit the test run loop when no more updates are deferred,
468 // meaining th echeck for updates did not cause anything to change so no new
469 // transition animations were started.
470 if (run_loop_for_test_
.get())
471 run_loop_for_test_
->Quit();
474 void MessagePopupCollection::OnDisplayMetricsChanged(
475 const gfx::Display
& display
) {
476 alignment_delegate_
->RecomputeAlignment(display
);
479 views::Widget
* MessagePopupCollection::GetWidgetForTest(const std::string
& id
)
481 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
483 if ((*iter
)->id() == id
)
484 return (*iter
)->GetWidget();
489 void MessagePopupCollection::CreateRunLoopForTest() {
490 run_loop_for_test_
.reset(new base::RunLoop());
493 void MessagePopupCollection::WaitForTest() {
494 run_loop_for_test_
->Run();
495 run_loop_for_test_
.reset();
498 gfx::Rect
MessagePopupCollection::GetToastRectAt(size_t index
) const {
499 DCHECK(defer_counter_
== 0) << "Fetching the bounds with animations active.";
501 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
504 views::Widget
* widget
= (*iter
)->GetWidget();
506 return widget
->GetWindowBoundsInScreen();
513 } // namespace message_center