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/toast_contents_view.h"
28 #include "ui/views/background.h"
29 #include "ui/views/layout/fill_layout.h"
30 #include "ui/views/view.h"
31 #include "ui/views/views_delegate.h"
32 #include "ui/views/widget/widget.h"
33 #include "ui/views/widget/widget_delegate.h"
35 namespace message_center
{
38 // Timeout between the last user-initiated close of the toast and the moment
39 // when normal layout/update of the toast stack continues. If the last toast was
40 // just closed, the timeout is shorter.
41 const int kMouseExitedDeferTimeoutMs
= 200;
43 // The margin between messages (and between the anchor unless
44 // first_item_has_no_margin was specified).
45 const int kToastMarginY
= kMarginBetweenItems
;
46 #if defined(OS_CHROMEOS)
47 const int kToastMarginX
= 3;
49 const int kToastMarginX
= kMarginBetweenItems
;
53 // If there should be no margin for the first item, this value needs to be
54 // substracted to flush the message to the shelf (the width of the border +
56 const int kNoToastMarginBorderAndShadowOffset
= 2;
60 MessagePopupCollection::MessagePopupCollection(gfx::NativeView parent
,
61 MessageCenter
* message_center
,
62 MessageCenterTray
* tray
,
63 bool first_item_has_no_margin
)
65 message_center_(message_center
),
67 display_id_(gfx::Display::kInvalidDisplayID
),
70 latest_toast_entered_(NULL
),
71 user_is_closing_toasts_by_clicking_(false),
72 first_item_has_no_margin_(first_item_has_no_margin
),
73 context_menu_controller_(new MessageViewContextMenuController(this)),
75 DCHECK(message_center_
);
76 defer_timer_
.reset(new base::OneShotTimer
<MessagePopupCollection
>);
77 message_center_
->AddObserver(this);
80 MessagePopupCollection::~MessagePopupCollection() {
81 weak_factory_
.InvalidateWeakPtrs();
84 screen_
->RemoveObserver(this);
85 message_center_
->RemoveObserver(this);
90 void MessagePopupCollection::ClickOnNotification(
91 const std::string
& notification_id
) {
92 message_center_
->ClickOnNotification(notification_id
);
95 void MessagePopupCollection::RemoveNotification(
96 const std::string
& notification_id
,
98 message_center_
->RemoveNotification(notification_id
, by_user
);
101 scoped_ptr
<ui::MenuModel
> MessagePopupCollection::CreateMenuModel(
102 const NotifierId
& notifier_id
,
103 const base::string16
& display_source
) {
104 return tray_
->CreateNotificationMenuModel(notifier_id
, display_source
);
107 bool MessagePopupCollection::HasClickedListener(
108 const std::string
& notification_id
) {
109 return message_center_
->HasClickedListener(notification_id
);
112 void MessagePopupCollection::ClickOnNotificationButton(
113 const std::string
& notification_id
,
115 message_center_
->ClickOnNotificationButton(notification_id
, button_index
);
118 void MessagePopupCollection::MarkAllPopupsShown() {
119 std::set
<std::string
> closed_ids
= CloseAllWidgets();
120 for (std::set
<std::string
>::iterator iter
= closed_ids
.begin();
121 iter
!= closed_ids
.end(); iter
++) {
122 message_center_
->MarkSinglePopupAsShown(*iter
, false);
126 void MessagePopupCollection::UpdateWidgets() {
127 NotificationList::PopupNotifications popups
=
128 message_center_
->GetPopupNotifications();
130 if (popups
.empty()) {
135 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
136 int base
= GetBaseLine(toasts_
.empty() ? NULL
: toasts_
.back());
138 // Iterate in the reverse order to keep the oldest toasts on screen. Newer
139 // items may be ignored if there are no room to place them.
140 for (NotificationList::PopupNotifications::const_reverse_iterator iter
=
141 popups
.rbegin(); iter
!= popups
.rend(); ++iter
) {
142 if (FindToast((*iter
)->id()))
145 NotificationView
* view
=
146 NotificationView::Create(NULL
,
148 true); // Create top-level notification.
149 view
->set_context_menu_controller(context_menu_controller_
.get());
150 int view_height
= ToastContentsView::GetToastSizeForView(view
).height();
151 int height_available
= top_down
? work_area_
.bottom() - base
: base
;
153 if (height_available
- view_height
- kToastMarginY
< 0) {
158 ToastContentsView
* toast
=
159 new ToastContentsView((*iter
)->id(), weak_factory_
.GetWeakPtr());
160 // There will be no contents already since this is a new ToastContentsView.
161 toast
->SetContents(view
, /*a11y_feedback_for_updates=*/false);
162 toasts_
.push_back(toast
);
163 view
->set_controller(toast
);
165 gfx::Size preferred_size
= toast
->GetPreferredSize();
166 gfx::Point
origin(GetToastOriginX(gfx::Rect(preferred_size
)), base
);
167 // The toast slides in from the edge of the screen horizontally.
168 if (alignment_
& POPUP_ALIGNMENT_LEFT
)
169 origin
.set_x(origin
.x() - preferred_size
.width());
171 origin
.set_x(origin
.x() + preferred_size
.width());
173 origin
.set_y(origin
.y() + view_height
);
175 toast
->RevealWithAnimation(origin
);
177 // Shift the base line to be a few pixels above the last added toast or (few
178 // pixels below last added toast if top-aligned).
180 base
+= view_height
+ kToastMarginY
;
182 base
-= view_height
+ kToastMarginY
;
184 if (views::ViewsDelegate::views_delegate
) {
185 views::ViewsDelegate::views_delegate
->NotifyAccessibilityEvent(
186 toast
, ui::AX_EVENT_ALERT
);
189 message_center_
->DisplayedNotification(
190 (*iter
)->id(), message_center::DISPLAY_SOURCE_POPUP
);
194 void MessagePopupCollection::OnMouseEntered(ToastContentsView
* toast_entered
) {
195 // Sometimes we can get two MouseEntered/MouseExited in a row when animating
196 // toasts. So we need to keep track of which one is the currently active one.
197 latest_toast_entered_
= toast_entered
;
199 message_center_
->PausePopupTimers();
201 if (user_is_closing_toasts_by_clicking_
)
202 defer_timer_
->Stop();
205 void MessagePopupCollection::OnMouseExited(ToastContentsView
* toast_exited
) {
206 // If we're exiting a toast after entering a different toast, then ignore
208 if (toast_exited
!= latest_toast_entered_
)
210 latest_toast_entered_
= NULL
;
212 if (user_is_closing_toasts_by_clicking_
) {
215 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs
),
217 &MessagePopupCollection::OnDeferTimerExpired
);
219 message_center_
->RestartPopupTimers();
223 std::set
<std::string
> MessagePopupCollection::CloseAllWidgets() {
224 std::set
<std::string
> closed_toast_ids
;
226 while (!toasts_
.empty()) {
227 ToastContentsView
* toast
= toasts_
.front();
229 closed_toast_ids
.insert(toast
->id());
231 OnMouseExited(toast
);
233 // CloseWithAnimation will cause the toast to forget about |this| so it is
234 // required when we forget a toast.
235 toast
->CloseWithAnimation();
238 return closed_toast_ids
;
241 void MessagePopupCollection::ForgetToast(ToastContentsView
* toast
) {
242 toasts_
.remove(toast
);
243 OnMouseExited(toast
);
246 void MessagePopupCollection::RemoveToast(ToastContentsView
* toast
,
247 bool mark_as_shown
) {
250 toast
->CloseWithAnimation();
253 message_center_
->MarkSinglePopupAsShown(toast
->id(), false);
256 int MessagePopupCollection::GetToastOriginX(const gfx::Rect
& toast_bounds
)
258 #if defined(OS_CHROMEOS)
259 // In ChromeOS, RTL UI language mirrors the whole desktop layout, so the toast
260 // widgets should be at the bottom-left instead of bottom right.
261 if (base::i18n::IsRTL())
262 return work_area_
.x() + kToastMarginX
;
264 if (alignment_
& POPUP_ALIGNMENT_LEFT
)
265 return work_area_
.x() + kToastMarginX
;
266 return work_area_
.right() - kToastMarginX
- toast_bounds
.width();
269 void MessagePopupCollection::RepositionWidgets() {
270 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
271 int base
= GetBaseLine(NULL
); // We don't want to position relative to last
272 // toast - we want re-position.
274 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();) {
275 Toasts::const_iterator curr
= iter
++;
276 gfx::Rect
bounds((*curr
)->bounds());
277 bounds
.set_x(GetToastOriginX(bounds
));
278 bounds
.set_y(alignment_
& POPUP_ALIGNMENT_TOP
? base
279 : base
- bounds
.height());
281 // The notification may scrolls the boundary of the screen due to image
282 // load and such notifications should disappear. Do not call
283 // CloseWithAnimation, we don't want to show the closing animation, and we
284 // don't want to mark such notifications as shown. See crbug.com/233424
285 if ((top_down
? work_area_
.bottom() - bounds
.bottom() : bounds
.y()) >= 0)
286 (*curr
)->SetBoundsWithAnimation(bounds
);
288 RemoveToast(*curr
, /*mark_as_shown=*/false);
290 // Shift the base line to be a few pixels above the last added toast or (few
291 // pixels below last added toast if top-aligned).
293 base
+= bounds
.height() + kToastMarginY
;
295 base
-= bounds
.height() + kToastMarginY
;
299 void MessagePopupCollection::RepositionWidgetsWithTarget() {
303 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
305 // Nothing to do if there are no widgets above target if bottom-aligned or no
306 // widgets below target if top-aligned.
307 if (top_down
? toasts_
.back()->origin().y() < target_top_edge_
308 : toasts_
.back()->origin().y() > target_top_edge_
)
311 Toasts::reverse_iterator iter
= toasts_
.rbegin();
312 for (; iter
!= toasts_
.rend(); ++iter
) {
313 // We only reposition widgets above target if bottom-aligned or widgets
314 // below target if top-aligned.
315 if (top_down
? (*iter
)->origin().y() < target_top_edge_
316 : (*iter
)->origin().y() > target_top_edge_
)
321 // Slide length is the number of pixels the widgets should move so that their
322 // bottom edge (top-edge if top-aligned) touches the target.
323 int slide_length
= std::abs(target_top_edge_
- (*iter
)->origin().y());
325 gfx::Rect
bounds((*iter
)->bounds());
327 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
328 // shift them downwards by slide_length.
330 bounds
.set_y(bounds
.y() - slide_length
);
332 bounds
.set_y(bounds
.y() + slide_length
);
333 (*iter
)->SetBoundsWithAnimation(bounds
);
335 if (iter
== toasts_
.rbegin())
340 void MessagePopupCollection::ComputePopupAlignment(gfx::Rect work_area
,
341 gfx::Rect screen_bounds
) {
342 // If the taskbar is at the top, render notifications top down. Some platforms
343 // like Gnome can have taskbars at top and bottom. In this case it's more
344 // likely that the systray is on the top one.
345 alignment_
= work_area
.y() > screen_bounds
.y() ? POPUP_ALIGNMENT_TOP
346 : POPUP_ALIGNMENT_BOTTOM
;
348 // If the taskbar is on the left show the notifications on the left. Otherwise
349 // show it on right since it's very likely that the systray is on the right if
350 // the taskbar is on the top or bottom.
351 // Since on some platforms like Ubuntu Unity there's also a launcher along
352 // with a taskbar (panel), we need to check that there is really nothing at
353 // the top before concluding that the taskbar is at the left.
354 alignment_
= static_cast<PopupAlignment
>(
356 ((work_area
.x() > screen_bounds
.x() && work_area
.y() == screen_bounds
.y())
357 ? POPUP_ALIGNMENT_LEFT
358 : POPUP_ALIGNMENT_RIGHT
));
361 int MessagePopupCollection::GetBaseLine(ToastContentsView
* last_toast
) const {
362 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
367 base
= work_area_
.y();
368 if (!first_item_has_no_margin_
)
369 base
+= kToastMarginY
;
371 base
-= kNoToastMarginBorderAndShadowOffset
;
373 base
= toasts_
.back()->bounds().bottom() + kToastMarginY
;
377 base
= work_area_
.bottom();
378 if (!first_item_has_no_margin_
)
379 base
-= kToastMarginY
;
381 base
+= kNoToastMarginBorderAndShadowOffset
;
383 base
= toasts_
.back()->origin().y() - kToastMarginY
;
389 void MessagePopupCollection::OnNotificationAdded(
390 const std::string
& notification_id
) {
391 DoUpdateIfPossible();
394 void MessagePopupCollection::OnNotificationRemoved(
395 const std::string
& notification_id
,
398 Toasts::const_iterator iter
= toasts_
.begin();
399 for (; iter
!= toasts_
.end(); ++iter
) {
400 if ((*iter
)->id() == notification_id
)
403 if (iter
== toasts_
.end())
406 target_top_edge_
= (*iter
)->bounds().y();
407 if (by_user
&& !user_is_closing_toasts_by_clicking_
) {
408 // [Re] start a timeout after which the toasts re-position to their
409 // normal locations after tracking the mouse pointer for easy deletion.
410 // This provides a period of time when toasts are easy to remove because
411 // they re-position themselves to have Close button right under the mouse
412 // pointer. If the user continue to remove the toasts, the delay is reset.
413 // Once user stopped removing the toasts, the toasts re-populate/rearrange
414 // after the specified delay.
415 user_is_closing_toasts_by_clicking_
= true;
416 IncrementDeferCounter();
419 // CloseWithAnimation ultimately causes a call to RemoveToast, which calls
420 // OnMouseExited. This means that |user_is_closing_toasts_by_clicking_| must
421 // have been set before this call, otherwise it will remain true even after
422 // the toast is closed, since the defer timer won't be started.
423 RemoveToast(*iter
, /*mark_as_shown=*/true);
426 RepositionWidgetsWithTarget();
429 void MessagePopupCollection::OnDeferTimerExpired() {
430 user_is_closing_toasts_by_clicking_
= false;
431 DecrementDeferCounter();
433 message_center_
->RestartPopupTimers();
436 void MessagePopupCollection::OnNotificationUpdated(
437 const std::string
& notification_id
) {
439 Toasts::const_iterator toast_iter
= toasts_
.begin();
440 for (; toast_iter
!= toasts_
.end(); ++toast_iter
) {
441 if ((*toast_iter
)->id() == notification_id
)
444 if (toast_iter
== toasts_
.end())
447 NotificationList::PopupNotifications notifications
=
448 message_center_
->GetPopupNotifications();
449 bool updated
= false;
451 for (NotificationList::PopupNotifications::iterator iter
=
452 notifications
.begin(); iter
!= notifications
.end(); ++iter
) {
453 Notification
* notification
= *iter
;
454 DCHECK(notification
);
455 ToastContentsView
* toast_contents_view
= *toast_iter
;
456 DCHECK(toast_contents_view
);
458 if (notification
->id() != notification_id
)
461 const RichNotificationData
& optional_fields
=
462 notification
->rich_notification_data();
463 bool a11y_feedback_for_updates
=
464 optional_fields
.should_make_spoken_feedback_for_popup_updates
;
466 toast_contents_view
->UpdateContents(*notification
,
467 a11y_feedback_for_updates
);
472 // OnNotificationUpdated() can be called when a notification is excluded from
473 // the popup notification list but still remains in the full notification
474 // list. In that case the widget for the notification has to be closed here.
476 RemoveToast(*toast_iter
, /*mark_as_shown=*/true);
478 if (user_is_closing_toasts_by_clicking_
)
479 RepositionWidgetsWithTarget();
481 DoUpdateIfPossible();
484 ToastContentsView
* MessagePopupCollection::FindToast(
485 const std::string
& notification_id
) const {
486 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
488 if ((*iter
)->id() == notification_id
)
494 void MessagePopupCollection::IncrementDeferCounter() {
498 void MessagePopupCollection::DecrementDeferCounter() {
500 DCHECK(defer_counter_
>= 0);
501 DoUpdateIfPossible();
504 // This is the main sequencer of tasks. It does a step, then waits for
505 // all started transitions to play out before doing the next step.
506 // First, remove all expired toasts.
507 // Then, reposition widgets (the reposition on close happens before all
508 // deferred tasks are even able to run)
509 // Then, see if there is vacant space for new toasts.
510 void MessagePopupCollection::DoUpdateIfPossible() {
512 gfx::Display display
;
514 // On Win+Aura, we don't have a parent since the popups currently show up
515 // on the Windows desktop, not in the Aura/Ash desktop. This code will
516 // display the popups on the primary display.
517 screen_
= gfx::Screen::GetNativeScreen();
518 display
= screen_
->GetPrimaryDisplay();
520 screen_
= gfx::Screen::GetScreenFor(parent_
);
521 display
= screen_
->GetDisplayNearestWindow(parent_
);
523 screen_
->AddObserver(this);
525 display_id_
= display
.id();
526 // |work_area_| can be set already and it should not be overwritten here.
527 if (work_area_
.IsEmpty()) {
528 work_area_
= display
.work_area();
529 ComputePopupAlignment(work_area_
, display
.bounds());
533 if (defer_counter_
> 0)
538 if (defer_counter_
> 0)
541 // Reposition could create extra space which allows additional widgets.
544 if (defer_counter_
> 0)
547 // Test support. Quit the test run loop when no more updates are deferred,
548 // meaining th echeck for updates did not cause anything to change so no new
549 // transition animations were started.
550 if (run_loop_for_test_
.get())
551 run_loop_for_test_
->Quit();
554 void MessagePopupCollection::SetDisplayInfo(const gfx::Rect
& work_area
,
555 const gfx::Rect
& screen_bounds
) {
556 if (work_area_
== work_area
)
559 work_area_
= work_area
;
560 ComputePopupAlignment(work_area
, screen_bounds
);
564 void MessagePopupCollection::OnDisplayAdded(const gfx::Display
& new_display
) {
567 void MessagePopupCollection::OnDisplayRemoved(const gfx::Display
& old_display
) {
568 if (display_id_
== old_display
.id() && !parent_
) {
569 gfx::Display display
= gfx::Screen::GetNativeScreen()->GetPrimaryDisplay();
570 display_id_
= display
.id();
571 SetDisplayInfo(display
.work_area(), display
.bounds());
575 void MessagePopupCollection::OnDisplayMetricsChanged(
576 const gfx::Display
& display
, uint32_t metrics
) {
577 if (display
.id() != display_id_
)
580 if (metrics
& DISPLAY_METRIC_BOUNDS
|| metrics
& DISPLAY_METRIC_WORK_AREA
)
581 SetDisplayInfo(display
.work_area(), display
.bounds());
584 views::Widget
* MessagePopupCollection::GetWidgetForTest(const std::string
& id
)
586 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
588 if ((*iter
)->id() == id
)
589 return (*iter
)->GetWidget();
594 void MessagePopupCollection::CreateRunLoopForTest() {
595 run_loop_for_test_
.reset(new base::RunLoop());
598 void MessagePopupCollection::WaitForTest() {
599 run_loop_for_test_
->Run();
600 run_loop_for_test_
.reset();
603 gfx::Rect
MessagePopupCollection::GetToastRectAt(size_t index
) const {
604 DCHECK(defer_counter_
== 0) << "Fetching the bounds with animations active.";
606 for (Toasts::const_iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();
609 views::Widget
* widget
= (*iter
)->GetWidget();
611 return widget
->GetWindowBoundsInScreen();
618 } // namespace message_center