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/base/animation/animation_delegate.h"
18 #include "ui/base/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 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end(); ++iter
) {
104 if ((*iter
) == toast
) {
111 void MessagePopupCollection::UpdateWidgets() {
112 NotificationList::PopupNotifications popups
=
113 message_center_
->GetPopupNotifications();
115 if (popups
.empty()) {
120 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
121 int base
= GetBaseLine(toasts_
.empty() ? NULL
: toasts_
.back());
123 // Iterate in the reverse order to keep the oldest toasts on screen. Newer
124 // items may be ignored if there are no room to place them.
125 for (NotificationList::PopupNotifications::const_reverse_iterator iter
=
126 popups
.rbegin(); iter
!= popups
.rend(); ++iter
) {
127 if (FindToast((*iter
)->id()))
131 NotificationView::Create(*(*iter
),
134 true, // Create expanded.
135 true); // Create top-level notification.
136 int view_height
= ToastContentsView::GetToastSizeForView(view
).height();
137 int height_available
= top_down
? work_area_
.bottom() - base
: base
;
139 if (height_available
- view_height
- kToastMarginY
< 0) {
144 ToastContentsView
* toast
= new ToastContentsView(
145 *iter
, AsWeakPtr(), message_center_
);
146 toast
->CreateWidget(parent_
);
147 toast
->SetContents(view
);
148 toasts_
.push_back(toast
);
150 gfx::Size preferred_size
= toast
->GetPreferredSize();
152 GetToastOriginX(gfx::Rect(preferred_size
)) + preferred_size
.width(),
153 top_down
? base
+ view_height
: base
);
154 toast
->RevealWithAnimation(origin
);
156 // Shift the base line to be a few pixels above the last added toast or (few
157 // pixels below last added toast if top-aligned).
159 base
+= view_height
+ kToastMarginY
;
161 base
-= view_height
+ kToastMarginY
;
163 message_center_
->DisplayedNotification((*iter
)->id());
164 if (views::ViewsDelegate::views_delegate
) {
165 views::ViewsDelegate::views_delegate
->NotifyAccessibilityEvent(
166 toast
, ui::AccessibilityTypes::EVENT_ALERT
);
171 void MessagePopupCollection::OnMouseEntered(ToastContentsView
* toast_entered
) {
172 // Sometimes we can get two MouseEntered/MouseExited in a row when animating
173 // toasts. So we need to keep track of which one is the currently active one.
174 latest_toast_entered_
= toast_entered
;
176 message_center_
->PausePopupTimers();
178 if (user_is_closing_toasts_by_clicking_
)
179 defer_timer_
->Stop();
182 void MessagePopupCollection::OnMouseExited(ToastContentsView
* toast_exited
) {
183 // If we're exiting a toast after entering a different toast, then ignore
185 if (toast_exited
!= latest_toast_entered_
)
187 latest_toast_entered_
= NULL
;
189 if (user_is_closing_toasts_by_clicking_
) {
192 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs
),
194 &MessagePopupCollection::OnDeferTimerExpired
);
196 message_center_
->RestartPopupTimers();
200 void MessagePopupCollection::CloseAllWidgets() {
201 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();) {
202 // the toast can be removed from toasts_ during CloseWithAnimation().
203 Toasts::iterator curiter
= iter
++;
204 (*curiter
)->CloseWithAnimation(true);
206 DCHECK(toasts_
.empty());
209 int MessagePopupCollection::GetToastOriginX(const gfx::Rect
& toast_bounds
) {
210 #if defined(OS_CHROMEOS)
211 // In ChromeOS, RTL UI language mirrors the whole desktop layout, so the toast
212 // widgets should be at the bottom-left instead of bottom right.
213 if (base::i18n::IsRTL())
214 return work_area_
.x() + kToastMarginX
;
216 if (alignment_
& POPUP_ALIGNMENT_LEFT
)
217 return work_area_
.x() + kToastMarginX
;
218 return work_area_
.right() - kToastMarginX
- toast_bounds
.width();
221 void MessagePopupCollection::RepositionWidgets() {
222 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
223 int base
= GetBaseLine(NULL
); // We don't want to position relative to last
224 // toast - we want re-position.
226 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end();) {
227 Toasts::iterator curr
= iter
++;
228 gfx::Rect
bounds((*curr
)->bounds());
229 bounds
.set_x(GetToastOriginX(bounds
));
230 bounds
.set_y(alignment_
& POPUP_ALIGNMENT_TOP
? base
231 : base
- bounds
.height());
233 // The notification may scrolls the boundary of the screen due to image
234 // load and such notifications should disappear. Do not call
235 // CloseWithAnimation, we don't want to show the closing animation, and we
236 // don't want to mark such notifications as shown. See crbug.com/233424
237 if ((top_down
? work_area_
.bottom() - bounds
.bottom() : bounds
.y()) >= 0)
238 (*curr
)->SetBoundsWithAnimation(bounds
);
240 (*curr
)->CloseWithAnimation(false);
242 // Shift the base line to be a few pixels above the last added toast or (few
243 // pixels below last added toast if top-aligned).
245 base
+= bounds
.height() + kToastMarginY
;
247 base
-= bounds
.height() + kToastMarginY
;
251 void MessagePopupCollection::RepositionWidgetsWithTarget() {
255 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
257 // Nothing to do if there are no widgets above target if bottom-aligned or no
258 // widgets below target if top-aligned.
259 if (top_down
? toasts_
.back()->origin().y() < target_top_edge_
260 : toasts_
.back()->origin().y() > target_top_edge_
)
263 Toasts::reverse_iterator iter
= toasts_
.rbegin();
264 for (; iter
!= toasts_
.rend(); ++iter
) {
265 // We only reposition widgets above target if bottom-aligned or widgets
266 // below target if top-aligned.
267 if (top_down
? (*iter
)->origin().y() < target_top_edge_
268 : (*iter
)->origin().y() > target_top_edge_
)
273 // Slide length is the number of pixels the widgets should move so that their
274 // bottom edge (top-edge if top-aligned) touches the target.
275 int slide_length
= std::abs(target_top_edge_
- (*iter
)->origin().y());
277 gfx::Rect
bounds((*iter
)->bounds());
279 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
280 // shift them downwards by slide_length.
282 bounds
.set_y(bounds
.y() - slide_length
);
284 bounds
.set_y(bounds
.y() + slide_length
);
285 (*iter
)->SetBoundsWithAnimation(bounds
);
287 if (iter
== toasts_
.rbegin())
292 void MessagePopupCollection::ComputePopupAlignment(gfx::Rect work_area
,
293 gfx::Rect screen_bounds
) {
294 // If the taskbar is at the top, render notifications top down. Some platforms
295 // like Gnome can have taskbars at top and bottom. In this case it's more
296 // likely that the systray is on the top one.
297 alignment_
= work_area
.y() > screen_bounds
.y() ? POPUP_ALIGNMENT_TOP
298 : POPUP_ALIGNMENT_BOTTOM
;
300 // If the taskbar is on the left show the notifications on the left. Otherwise
301 // show it on right since it's very likely that the systray is on the right if
302 // the taskbar is on the top or bottom.
303 // Since on some platforms like Ubuntu Unity there's also a launcher along
304 // with a taskbar (panel), we need to check that there is really nothing at
305 // the top before concluding that the taskbar is at the left.
306 alignment_
= static_cast<PopupAlignment
>(
308 ((work_area
.x() > screen_bounds
.x() && work_area
.y() == screen_bounds
.y())
309 ? POPUP_ALIGNMENT_LEFT
310 : POPUP_ALIGNMENT_RIGHT
));
313 int MessagePopupCollection::GetBaseLine(ToastContentsView
* last_toast
) {
314 bool top_down
= alignment_
& POPUP_ALIGNMENT_TOP
;
319 base
= work_area_
.y();
320 if (!first_item_has_no_margin_
)
321 base
+= kToastMarginY
;
323 base
-= kNoToastMarginBorderAndShadowOffset
;
325 base
= toasts_
.back()->bounds().bottom() + kToastMarginY
;
329 base
= work_area_
.bottom();
330 if (!first_item_has_no_margin_
)
331 base
-= kToastMarginY
;
333 base
+= kNoToastMarginBorderAndShadowOffset
;
335 base
= toasts_
.back()->origin().y() - kToastMarginY
;
341 void MessagePopupCollection::OnNotificationAdded(
342 const std::string
& notification_id
) {
343 DoUpdateIfPossible();
346 void MessagePopupCollection::OnNotificationRemoved(
347 const std::string
& notification_id
,
350 Toasts::iterator iter
= toasts_
.begin();
351 for (; iter
!= toasts_
.end(); ++iter
) {
352 if ((*iter
)->id() == notification_id
)
355 if (iter
== toasts_
.end())
358 target_top_edge_
= (*iter
)->bounds().y();
359 (*iter
)->CloseWithAnimation(true);
361 RepositionWidgetsWithTarget();
362 // [Re] start a timeout after which the toasts re-position to their
363 // normal locations after tracking the mouse pointer for easy deletion.
364 // This provides a period of time when toasts are easy to remove because
365 // they re-position themselves to have Close button right under the mouse
366 // pointer. If the user continue to remove the toasts, the delay is reset.
367 // Once user stopped removing the toasts, the toasts re-populate/rearrange
368 // after the specified delay.
369 if (!user_is_closing_toasts_by_clicking_
) {
370 user_is_closing_toasts_by_clicking_
= true;
371 IncrementDeferCounter();
376 void MessagePopupCollection::OnDeferTimerExpired() {
377 user_is_closing_toasts_by_clicking_
= false;
378 DecrementDeferCounter();
380 message_center_
->RestartPopupTimers();
383 void MessagePopupCollection::OnNotificationUpdated(
384 const std::string
& notification_id
) {
386 Toasts::iterator toast_iter
= toasts_
.begin();
387 for (; toast_iter
!= toasts_
.end(); ++toast_iter
) {
388 if ((*toast_iter
)->id() == notification_id
)
391 if (toast_iter
== toasts_
.end())
394 NotificationList::PopupNotifications notifications
=
395 message_center_
->GetPopupNotifications();
396 bool updated
= false;
398 for (NotificationList::PopupNotifications::iterator iter
=
399 notifications
.begin(); iter
!= notifications
.end(); ++iter
) {
400 if ((*iter
)->id() != notification_id
)
404 NotificationView::Create(*(*iter
),
407 true, // Create expanded.
408 true); // Create top-level notification.
409 (*toast_iter
)->SetContents(view
);
413 // OnNotificationUpdated() can be called when a notification is excluded from
414 // the popup notification list but still remains in the full notification
415 // list. In that case the widget for the notification has to be closed here.
417 (*toast_iter
)->CloseWithAnimation(true);
419 if (user_is_closing_toasts_by_clicking_
)
420 RepositionWidgetsWithTarget();
422 DoUpdateIfPossible();
425 ToastContentsView
* MessagePopupCollection::FindToast(
426 const std::string
& notification_id
) {
427 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end(); ++iter
) {
428 if ((*iter
)->id() == notification_id
)
434 void MessagePopupCollection::IncrementDeferCounter() {
438 void MessagePopupCollection::DecrementDeferCounter() {
440 DCHECK(defer_counter_
>= 0);
441 DoUpdateIfPossible();
444 // This is the main sequencer of tasks. It does a step, then waits for
445 // all started transitions to play out before doing the next step.
446 // First, remove all expired toasts.
447 // Then, reposition widgets (the reposition on close happens before all
448 // deferred tasks are even able to run)
449 // Then, see if there is vacant space for new toasts.
450 void MessagePopupCollection::DoUpdateIfPossible() {
451 if (defer_counter_
> 0)
456 if (defer_counter_
> 0)
459 // Reposition could create extra space which allows additional widgets.
462 if (defer_counter_
> 0)
465 // Test support. Quit the test run loop when no more updates are deferred,
466 // meaining th echeck for updates did not cause anything to change so no new
467 // transition animations were started.
468 if (run_loop_for_test_
.get())
469 run_loop_for_test_
->Quit();
472 void MessagePopupCollection::SetDisplayInfo(const gfx::Rect
& work_area
,
473 const gfx::Rect
& screen_bounds
) {
474 if (work_area_
== work_area
)
477 work_area_
= work_area
;
478 ComputePopupAlignment(work_area
, screen_bounds
);
482 void MessagePopupCollection::OnDisplayBoundsChanged(
483 const gfx::Display
& display
) {
484 if (display
.id() != display_id_
)
487 SetDisplayInfo(display
.work_area(), display
.bounds());
490 void MessagePopupCollection::OnDisplayAdded(const gfx::Display
& new_display
) {
493 void MessagePopupCollection::OnDisplayRemoved(const gfx::Display
& old_display
) {
496 views::Widget
* MessagePopupCollection::GetWidgetForTest(const std::string
& id
) {
497 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end(); ++iter
) {
498 if ((*iter
)->id() == id
)
499 return (*iter
)->GetWidget();
504 void MessagePopupCollection::RunLoopForTest() {
505 run_loop_for_test_
.reset(new base::RunLoop());
506 run_loop_for_test_
->Run();
507 run_loop_for_test_
.reset();
510 gfx::Rect
MessagePopupCollection::GetToastRectAt(size_t index
) {
511 DCHECK(defer_counter_
== 0) << "Fetching the bounds with animations active.";
513 for (Toasts::iterator iter
= toasts_
.begin(); iter
!= toasts_
.end(); ++iter
) {
515 views::Widget
* widget
= (*iter
)->GetWidget();
517 return widget
->GetWindowBoundsInScreen();
524 } // namespace message_center