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/base/accessibility/accessibility_types.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/notification.h"
23 #include "ui/message_center/notification_list.h"
24 #include "ui/message_center/views/notification_view.h"
25 #include "ui/message_center/views/toast_contents_view.h"
26 #include "ui/views/background.h"
27 #include "ui/views/layout/fill_layout.h"
28 #include "ui/views/view.h"
29 #include "ui/views/views_delegate.h"
30 #include "ui/views/widget/widget.h"
31 #include "ui/views/widget/widget_delegate.h"
33 namespace message_center
{
36 // Timeout between the last user-initiated close of the toast and the moment
37 // when normal layout/update of the toast stack continues. If the last toast was
38 // just closed, the timeout is shorter.
39 const int kMouseExitedDeferTimeoutMs
= 200;
41 // The margin between messages (and between the anchor unless
42 // first_item_has_no_margin was specified).
43 const int kToastMarginY
= kMarginBetweenItems
;
44 #if defined(OS_CHROMEOS)
45 const int kToastMarginX
= 3;
47 const int kToastMarginX
= kMarginBetweenItems
;
51 // If there should be no margin for the first item, this value needs to be
52 // substracted to flush the message to the shelf (the width of the border +
54 const int kNoToastMarginBorderAndShadowOffset
= 2;
58 MessagePopupCollection::MessagePopupCollection(gfx::NativeView parent
,
59 MessageCenter
* message_center
,
60 MessageCenterTray
* tray
,
61 bool first_item_has_no_margin
)
63 message_center_(message_center
),
66 latest_toast_entered_(NULL
),
67 user_is_closing_toasts_by_clicking_(false),
68 first_item_has_no_margin_(first_item_has_no_margin
) {
69 DCHECK(message_center_
);
70 defer_timer_
.reset(new base::OneShotTimer
<MessagePopupCollection
>);
71 message_center_
->AddObserver(this);
72 gfx::Screen
* screen
= NULL
;
75 // On Win+Aura, we don't have a parent since the popups currently show up
76 // on the Windows desktop, not in the Aura/Ash desktop. This code will
77 // display the popups on the primary display.
78 screen
= gfx::Screen::GetNativeScreen();
79 display
= screen
->GetPrimaryDisplay();
81 screen
= gfx::Screen::GetScreenFor(parent_
);
82 display
= screen
->GetDisplayNearestWindow(parent_
);
84 screen
->AddObserver(this);
86 display_id_
= display
.id();
87 work_area_
= display
.work_area();
88 ComputePopupAlignment(work_area_
, display
.bounds());
90 // We should not update before work area and popup alignment are computed.
94 MessagePopupCollection::~MessagePopupCollection() {
95 gfx::Screen
* screen
= parent_
?
96 gfx::Screen::GetScreenFor(parent_
) : gfx::Screen::GetNativeScreen();
97 screen
->RemoveObserver(this);
98 message_center_
->RemoveObserver(this);
102 void MessagePopupCollection::RemoveToast(ToastContentsView
* toast
) {
103 OnMouseExited(toast
);
105 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end(); ++iter
) {
106 if ((*iter
) == toast
) {
113 void MessagePopupCollection::UpdateWidgets() {
114 NotificationList::PopupNotifications popups
=
115 message_center_
->GetPopupNotifications();
117 if (popups
.empty()) {
122 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
123 int base
= GetBaseLine(toasts_
.empty() ? NULL
: toasts_
.back());
125 // Iterate in the reverse order to keep the oldest toasts on screen. Newer
126 // items may be ignored if there are no room to place them.
127 for (NotificationList::PopupNotifications::const_reverse_iterator iter
=
128 popups
.rbegin(); iter
!= popups
.rend(); ++iter
) {
129 if (FindToast((*iter
)->id()))
133 NotificationView::Create(*(*iter
),
136 true, // Create expanded.
137 true); // Create top-level notification.
138 int view_height
= ToastContentsView::GetToastSizeForView(view
).height();
139 int height_available
= top_down
? work_area_
.bottom() - base
: base
;
141 if (height_available
- view_height
- kToastMarginY
< 0) {
146 ToastContentsView
* toast
= new ToastContentsView(
147 *iter
, AsWeakPtr(), message_center_
);
148 toast
->CreateWidget(parent_
);
149 toast
->SetContents(view
);
150 toasts_
.push_back(toast
);
152 gfx::Size preferred_size
= toast
->GetPreferredSize();
153 gfx::Point
origin(GetToastOriginX(gfx::Rect(preferred_size
)), base
);
154 // The toast slides in from the edge of the screen horizontally.
155 if (alignment_
& POPUP_ALIGNMENT_LEFT
)
156 origin
.set_x(origin
.x() - preferred_size
.width());
158 origin
.set_x(origin
.x() + preferred_size
.width());
160 origin
.set_y(origin
.y() + view_height
);
161 toast
->RevealWithAnimation(origin
);
163 // Shift the base line to be a few pixels above the last added toast or (few
164 // pixels below last added toast if top-aligned).
166 base
+= view_height
+ kToastMarginY
;
168 base
-= view_height
+ kToastMarginY
;
170 message_center_
->DisplayedNotification((*iter
)->id());
171 if (views::ViewsDelegate::views_delegate
) {
172 views::ViewsDelegate::views_delegate
->NotifyAccessibilityEvent(
173 toast
, ui::AccessibilityTypes::EVENT_ALERT
);
178 void MessagePopupCollection::OnMouseEntered(ToastContentsView
* toast_entered
) {
179 // Sometimes we can get two MouseEntered/MouseExited in a row when animating
180 // toasts. So we need to keep track of which one is the currently active one.
181 latest_toast_entered_
= toast_entered
;
183 message_center_
->PausePopupTimers();
185 if (user_is_closing_toasts_by_clicking_
)
186 defer_timer_
->Stop();
189 void MessagePopupCollection::OnMouseExited(ToastContentsView
* toast_exited
) {
190 // If we're exiting a toast after entering a different toast, then ignore
192 if (toast_exited
!= latest_toast_entered_
)
194 latest_toast_entered_
= NULL
;
196 if (user_is_closing_toasts_by_clicking_
) {
199 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs
),
201 &MessagePopupCollection::OnDeferTimerExpired
);
203 message_center_
->RestartPopupTimers();
207 void MessagePopupCollection::CloseAllWidgets() {
208 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();) {
209 // the toast can be removed from toasts_ during CloseWithAnimation().
210 Toasts::iterator curiter
= iter
++;
211 (*curiter
)->CloseWithAnimation(true);
213 DCHECK(toasts_
.empty());
216 int MessagePopupCollection::GetToastOriginX(const gfx::Rect
& toast_bounds
) {
217 #if defined(OS_CHROMEOS)
218 // In ChromeOS, RTL UI language mirrors the whole desktop layout, so the toast
219 // widgets should be at the bottom-left instead of bottom right.
220 if (base::i18n::IsRTL())
221 return work_area_
.x() + kToastMarginX
;
223 if (alignment_
& POPUP_ALIGNMENT_LEFT
)
224 return work_area_
.x() + kToastMarginX
;
225 return work_area_
.right() - kToastMarginX
- toast_bounds
.width();
228 void MessagePopupCollection::RepositionWidgets() {
229 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
230 int base
= GetBaseLine(NULL
); // We don't want to position relative to last
231 // toast - we want re-position.
233 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();) {
234 Toasts::iterator curr
= iter
++;
235 gfx::Rect
bounds((*curr
)->bounds());
236 bounds
.set_x(GetToastOriginX(bounds
));
237 bounds
.set_y(alignment_
& POPUP_ALIGNMENT_TOP
? base
238 : base
- bounds
.height());
240 // The notification may scrolls the boundary of the screen due to image
241 // load and such notifications should disappear. Do not call
242 // CloseWithAnimation, we don't want to show the closing animation, and we
243 // don't want to mark such notifications as shown. See crbug.com/233424
244 if ((top_down
? work_area_
.bottom() - bounds
.bottom() : bounds
.y()) >= 0)
245 (*curr
)->SetBoundsWithAnimation(bounds
);
247 (*curr
)->CloseWithAnimation(false);
249 // Shift the base line to be a few pixels above the last added toast or (few
250 // pixels below last added toast if top-aligned).
252 base
+= bounds
.height() + kToastMarginY
;
254 base
-= bounds
.height() + kToastMarginY
;
258 void MessagePopupCollection::RepositionWidgetsWithTarget() {
262 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
264 // Nothing to do if there are no widgets above target if bottom-aligned or no
265 // widgets below target if top-aligned.
266 if (top_down
? toasts_
.back()->origin().y() < target_top_edge_
267 : toasts_
.back()->origin().y() > target_top_edge_
)
270 Toasts::reverse_iterator iter
= toasts_
.rbegin();
271 for (; iter
!= toasts_
.rend(); ++iter
) {
272 // We only reposition widgets above target if bottom-aligned or widgets
273 // below target if top-aligned.
274 if (top_down
? (*iter
)->origin().y() < target_top_edge_
275 : (*iter
)->origin().y() > target_top_edge_
)
280 // Slide length is the number of pixels the widgets should move so that their
281 // bottom edge (top-edge if top-aligned) touches the target.
282 int slide_length
= std::abs(target_top_edge_
- (*iter
)->origin().y());
284 gfx::Rect
bounds((*iter
)->bounds());
286 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
287 // shift them downwards by slide_length.
289 bounds
.set_y(bounds
.y() - slide_length
);
291 bounds
.set_y(bounds
.y() + slide_length
);
292 (*iter
)->SetBoundsWithAnimation(bounds
);
294 if (iter
== toasts_
.rbegin())
299 void MessagePopupCollection::ComputePopupAlignment(gfx::Rect work_area
,
300 gfx::Rect screen_bounds
) {
301 // If the taskbar is at the top, render notifications top down. Some platforms
302 // like Gnome can have taskbars at top and bottom. In this case it's more
303 // likely that the systray is on the top one.
304 alignment_
= work_area
.y() > screen_bounds
.y() ? POPUP_ALIGNMENT_TOP
305 : POPUP_ALIGNMENT_BOTTOM
;
307 // If the taskbar is on the left show the notifications on the left. Otherwise
308 // show it on right since it's very likely that the systray is on the right if
309 // the taskbar is on the top or bottom.
310 // Since on some platforms like Ubuntu Unity there's also a launcher along
311 // with a taskbar (panel), we need to check that there is really nothing at
312 // the top before concluding that the taskbar is at the left.
313 alignment_
= static_cast<PopupAlignment
>(
315 ((work_area
.x() > screen_bounds
.x() && work_area
.y() == screen_bounds
.y())
316 ? POPUP_ALIGNMENT_LEFT
317 : POPUP_ALIGNMENT_RIGHT
));
320 int MessagePopupCollection::GetBaseLine(ToastContentsView
* last_toast
) {
321 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
326 base
= work_area_
.y();
327 if (!first_item_has_no_margin_
)
328 base
+= kToastMarginY
;
330 base
-= kNoToastMarginBorderAndShadowOffset
;
332 base
= toasts_
.back()->bounds().bottom() + kToastMarginY
;
336 base
= work_area_
.bottom();
337 if (!first_item_has_no_margin_
)
338 base
-= kToastMarginY
;
340 base
+= kNoToastMarginBorderAndShadowOffset
;
342 base
= toasts_
.back()->origin().y() - kToastMarginY
;
348 void MessagePopupCollection::OnNotificationAdded(
349 const std::string
& notification_id
) {
350 DoUpdateIfPossible();
353 void MessagePopupCollection::OnNotificationRemoved(
354 const std::string
& notification_id
,
357 Toasts::iterator iter
= toasts_
.begin();
358 for (; iter
!= toasts_
.end(); ++iter
) {
359 if ((*iter
)->id() == notification_id
)
362 if (iter
== toasts_
.end())
365 target_top_edge_
= (*iter
)->bounds().y();
366 if (by_user
&& !user_is_closing_toasts_by_clicking_
) {
367 // [Re] start a timeout after which the toasts re-position to their
368 // normal locations after tracking the mouse pointer for easy deletion.
369 // This provides a period of time when toasts are easy to remove because
370 // they re-position themselves to have Close button right under the mouse
371 // pointer. If the user continue to remove the toasts, the delay is reset.
372 // Once user stopped removing the toasts, the toasts re-populate/rearrange
373 // after the specified delay.
374 user_is_closing_toasts_by_clicking_
= true;
375 IncrementDeferCounter();
378 // CloseWithAnimation ultimately causes a call to RemoveToast, which calls
379 // OnMouseExited. This means that |user_is_closing_toasts_by_clicking_| must
380 // have been set before this call, otherwise it will remain true even after
381 // the toast is closed, since the defer timer won't be started.
382 (*iter
)->CloseWithAnimation(true);
385 RepositionWidgetsWithTarget();
388 void MessagePopupCollection::OnDeferTimerExpired() {
389 user_is_closing_toasts_by_clicking_
= false;
390 DecrementDeferCounter();
392 message_center_
->RestartPopupTimers();
395 void MessagePopupCollection::OnNotificationUpdated(
396 const std::string
& notification_id
) {
398 Toasts::iterator toast_iter
= toasts_
.begin();
399 for (; toast_iter
!= toasts_
.end(); ++toast_iter
) {
400 if ((*toast_iter
)->id() == notification_id
)
403 if (toast_iter
== toasts_
.end())
406 NotificationList::PopupNotifications notifications
=
407 message_center_
->GetPopupNotifications();
408 bool updated
= false;
410 for (NotificationList::PopupNotifications::iterator iter
=
411 notifications
.begin(); iter
!= notifications
.end(); ++iter
) {
412 if ((*iter
)->id() != notification_id
)
416 NotificationView::Create(*(*iter
),
419 true, // Create expanded.
420 true); // Create top-level notification.
421 (*toast_iter
)->SetContents(view
);
425 // OnNotificationUpdated() can be called when a notification is excluded from
426 // the popup notification list but still remains in the full notification
427 // list. In that case the widget for the notification has to be closed here.
429 (*toast_iter
)->CloseWithAnimation(true);
431 if (user_is_closing_toasts_by_clicking_
)
432 RepositionWidgetsWithTarget();
434 DoUpdateIfPossible();
437 ToastContentsView
* MessagePopupCollection::FindToast(
438 const std::string
& notification_id
) {
439 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end(); ++iter
) {
440 if ((*iter
)->id() == notification_id
)
446 void MessagePopupCollection::IncrementDeferCounter() {
450 void MessagePopupCollection::DecrementDeferCounter() {
452 DCHECK(defer_counter_
>= 0);
453 DoUpdateIfPossible();
456 // This is the main sequencer of tasks. It does a step, then waits for
457 // all started transitions to play out before doing the next step.
458 // First, remove all expired toasts.
459 // Then, reposition widgets (the reposition on close happens before all
460 // deferred tasks are even able to run)
461 // Then, see if there is vacant space for new toasts.
462 void MessagePopupCollection::DoUpdateIfPossible() {
463 if (defer_counter_
> 0)
468 if (defer_counter_
> 0)
471 // Reposition could create extra space which allows additional widgets.
474 if (defer_counter_
> 0)
477 // Test support. Quit the test run loop when no more updates are deferred,
478 // meaining th echeck for updates did not cause anything to change so no new
479 // transition animations were started.
480 if (run_loop_for_test_
.get())
481 run_loop_for_test_
->Quit();
484 void MessagePopupCollection::SetDisplayInfo(const gfx::Rect
& work_area
,
485 const gfx::Rect
& screen_bounds
) {
486 if (work_area_
== work_area
)
489 work_area_
= work_area
;
490 ComputePopupAlignment(work_area
, screen_bounds
);
494 void MessagePopupCollection::OnDisplayBoundsChanged(
495 const gfx::Display
& display
) {
496 if (display
.id() != display_id_
)
499 SetDisplayInfo(display
.work_area(), display
.bounds());
502 void MessagePopupCollection::OnDisplayAdded(const gfx::Display
& new_display
) {
505 void MessagePopupCollection::OnDisplayRemoved(const gfx::Display
& old_display
) {
508 views::Widget
* MessagePopupCollection::GetWidgetForTest(const std::string
& id
) {
509 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end(); ++iter
) {
510 if ((*iter
)->id() == id
)
511 return (*iter
)->GetWidget();
516 void MessagePopupCollection::RunLoopForTest() {
517 run_loop_for_test_
.reset(new base::RunLoop());
518 run_loop_for_test_
->Run();
519 run_loop_for_test_
.reset();
522 gfx::Rect
MessagePopupCollection::GetToastRectAt(size_t index
) {
523 DCHECK(defer_counter_
== 0) << "Fetching the bounds with animations active.";
525 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end(); ++iter
) {
527 views::Widget
* widget
= (*iter
)->GetWidget();
529 return widget
->GetWindowBoundsInScreen();
536 } // namespace message_center