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 NotificationList::PopupNotifications popups
=
116 message_center_
->GetPopupNotifications();
118 if (popups
.empty()) {
123 bool top_down
= alignment_delegate_
->IsTopDown();
124 int base
= GetBaseLine(toasts_
.empty() ? NULL
: toasts_
.back());
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()))
133 NotificationView
* view
=
134 NotificationView::Create(NULL
,
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
;
142 if (height_available
- view_height
- kToastMarginY
< 0) {
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
);
154 gfx::Size preferred_size
= toast
->GetPreferredSize();
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());
161 origin
.set_x(origin
.x() + preferred_size
.width());
163 origin
.set_y(origin
.y() + view_height
);
165 toast
->RevealWithAnimation(origin
);
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).
170 base
+= view_height
+ kToastMarginY
;
172 base
-= view_height
+ kToastMarginY
;
174 if (views::ViewsDelegate::views_delegate
) {
175 views::ViewsDelegate::views_delegate
->NotifyAccessibilityEvent(
176 toast
, ui::AX_EVENT_ALERT
);
179 message_center_
->DisplayedNotification(
180 (*iter
)->id(), message_center::DISPLAY_SOURCE_POPUP
);
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
;
189 message_center_
->PausePopupTimers();
191 if (user_is_closing_toasts_by_clicking_
)
192 defer_timer_
->Stop();
195 void MessagePopupCollection::OnMouseExited(ToastContentsView
* toast_exited
) {
196 // If we're exiting a toast after entering a different toast, then ignore
198 if (toast_exited
!= latest_toast_entered_
)
200 latest_toast_entered_
= NULL
;
202 if (user_is_closing_toasts_by_clicking_
) {
205 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs
),
207 &MessagePopupCollection::OnDeferTimerExpired
);
209 message_center_
->RestartPopupTimers();
213 std::set
<std::string
> MessagePopupCollection::CloseAllWidgets() {
214 std::set
<std::string
> closed_toast_ids
;
216 while (!toasts_
.empty()) {
217 ToastContentsView
* toast
= toasts_
.front();
219 closed_toast_ids
.insert(toast
->id());
221 OnMouseExited(toast
);
223 // CloseWithAnimation will cause the toast to forget about |this| so it is
224 // required when we forget a toast.
225 toast
->CloseWithAnimation();
228 return closed_toast_ids
;
231 void MessagePopupCollection::ForgetToast(ToastContentsView
* toast
) {
232 toasts_
.remove(toast
);
233 OnMouseExited(toast
);
236 void MessagePopupCollection::RemoveToast(ToastContentsView
* toast
,
237 bool mark_as_shown
) {
240 toast
->CloseWithAnimation();
243 message_center_
->MarkSinglePopupAsShown(toast
->id(), false);
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.
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());
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()
263 (*curr
)->SetBoundsWithAnimation(bounds
);
265 RemoveToast(*curr
, /*mark_as_shown=*/false);
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).
270 base
+= bounds
.height() + kToastMarginY
;
272 base
-= bounds
.height() + kToastMarginY
;
276 void MessagePopupCollection::RepositionWidgetsWithTarget() {
280 bool top_down
= alignment_delegate_
->IsTopDown();
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_
)
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_
)
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());
302 gfx::Rect
bounds((*iter
)->bounds());
304 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
305 // shift them downwards by slide_length.
307 bounds
.set_y(bounds
.y() - slide_length
);
309 bounds
.set_y(bounds
.y() + slide_length
);
310 (*iter
)->SetBoundsWithAnimation(bounds
);
312 if (iter
== toasts_
.rbegin())
317 int MessagePopupCollection::GetBaseLine(ToastContentsView
* last_toast
) const {
319 return alignment_delegate_
->GetBaseLine();
320 } else if (alignment_delegate_
->IsTopDown()) {
321 return toasts_
.back()->bounds().bottom() + kToastMarginY
;
323 return toasts_
.back()->origin().y() - kToastMarginY
;
327 void MessagePopupCollection::OnNotificationAdded(
328 const std::string
& notification_id
) {
329 DoUpdateIfPossible();
332 void MessagePopupCollection::OnNotificationRemoved(
333 const std::string
& notification_id
,
336 Toasts::const_iterator iter
= toasts_
.begin();
337 for (; iter
!= toasts_
.end(); ++iter
) {
338 if ((*iter
)->id() == notification_id
)
341 if (iter
== toasts_
.end())
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();
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);
364 RepositionWidgetsWithTarget();
367 void MessagePopupCollection::OnDeferTimerExpired() {
368 user_is_closing_toasts_by_clicking_
= false;
369 DecrementDeferCounter();
371 message_center_
->RestartPopupTimers();
374 void MessagePopupCollection::OnNotificationUpdated(
375 const std::string
& notification_id
) {
377 Toasts::const_iterator toast_iter
= toasts_
.begin();
378 for (; toast_iter
!= toasts_
.end(); ++toast_iter
) {
379 if ((*toast_iter
)->id() == notification_id
)
382 if (toast_iter
== toasts_
.end())
385 NotificationList::PopupNotifications notifications
=
386 message_center_
->GetPopupNotifications();
387 bool updated
= false;
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
);
396 if (notification
->id() != notification_id
)
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
;
404 toast_contents_view
->UpdateContents(*notification
,
405 a11y_feedback_for_updates
);
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.
414 RemoveToast(*toast_iter
, /*mark_as_shown=*/true);
416 if (user_is_closing_toasts_by_clicking_
)
417 RepositionWidgetsWithTarget();
419 DoUpdateIfPossible();
422 ToastContentsView
* MessagePopupCollection::FindToast(
423 const std::string
& notification_id
) const {
424 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
426 if ((*iter
)->id() == notification_id
)
432 void MessagePopupCollection::IncrementDeferCounter() {
436 void MessagePopupCollection::DecrementDeferCounter() {
438 DCHECK(defer_counter_
>= 0);
439 DoUpdateIfPossible();
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.
448 void MessagePopupCollection::DoUpdateIfPossible() {
449 if (defer_counter_
> 0)
454 if (defer_counter_
> 0)
457 // Reposition could create extra space which allows additional widgets.
460 if (defer_counter_
> 0)
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();
470 void MessagePopupCollection::OnDisplayMetricsChanged(
471 const gfx::Display
& display
) {
472 alignment_delegate_
->RecomputeAlignment(display
);
475 views::Widget
* MessagePopupCollection::GetWidgetForTest(const std::string
& id
)
477 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
479 if ((*iter
)->id() == id
)
480 return (*iter
)->GetWidget();
485 void MessagePopupCollection::CreateRunLoopForTest() {
486 run_loop_for_test_
.reset(new base::RunLoop());
489 void MessagePopupCollection::WaitForTest() {
490 run_loop_for_test_
->Run();
491 run_loop_for_test_
.reset();
494 gfx::Rect
MessagePopupCollection::GetToastRectAt(size_t index
) const {
495 DCHECK(defer_counter_
== 0) << "Fetching the bounds with animations active.";
497 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
500 views::Widget
* widget
= (*iter
)->GetWidget();
502 return widget
->GetWindowBoundsInScreen();
509 } // namespace message_center