Update path of checkdeps to buildtools checkout
[chromium-blink-merge.git] / ui / message_center / views / message_popup_collection.cc
blobd1116d6f3464c4807448b30f638b2d966f15dac5
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"
7 #include <set>
9 #include "base/bind.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 {
36 namespace {
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;
48 #else
49 const int kToastMarginX = kMarginBetweenItems;
50 #endif
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 +
55 // shadow).
56 const int kNoToastMarginBorderAndShadowOffset = 2;
58 } // namespace.
60 MessagePopupCollection::MessagePopupCollection(gfx::NativeView parent,
61 MessageCenter* message_center,
62 MessageCenterTray* tray,
63 bool first_item_has_no_margin)
64 : parent_(parent),
65 message_center_(message_center),
66 tray_(tray),
67 display_id_(gfx::Display::kInvalidDisplayID),
68 screen_(NULL),
69 defer_counter_(0),
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)),
74 weak_factory_(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();
83 if (screen_)
84 screen_->RemoveObserver(this);
85 message_center_->RemoveObserver(this);
87 CloseAllWidgets();
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,
97 bool by_user) {
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,
114 int button_index) {
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()) {
131 CloseAllWidgets();
132 return;
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()))
143 continue;
145 NotificationView* view =
146 NotificationView::Create(NULL,
147 *(*iter),
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) {
154 delete view;
155 break;
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());
170 else
171 origin.set_x(origin.x() + preferred_size.width());
172 if (top_down)
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).
179 if (top_down)
180 base += view_height + kToastMarginY;
181 else
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
207 // this mouse event.
208 if (toast_exited != latest_toast_entered_)
209 return;
210 latest_toast_entered_ = NULL;
212 if (user_is_closing_toasts_by_clicking_) {
213 defer_timer_->Start(
214 FROM_HERE,
215 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs),
216 this,
217 &MessagePopupCollection::OnDeferTimerExpired);
218 } else {
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();
228 toasts_.pop_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) {
248 ForgetToast(toast);
250 toast->CloseWithAnimation();
252 if (mark_as_shown)
253 message_center_->MarkSinglePopupAsShown(toast->id(), false);
256 int MessagePopupCollection::GetToastOriginX(const gfx::Rect& toast_bounds)
257 const {
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;
263 #endif
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);
287 else
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).
292 if (top_down)
293 base += bounds.height() + kToastMarginY;
294 else
295 base -= bounds.height() + kToastMarginY;
299 void MessagePopupCollection::RepositionWidgetsWithTarget() {
300 if (toasts_.empty())
301 return;
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_)
309 return;
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_)
317 break;
319 --iter;
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());
324 for (;; --iter) {
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.
329 if (top_down)
330 bounds.set_y(bounds.y() - slide_length);
331 else
332 bounds.set_y(bounds.y() + slide_length);
333 (*iter)->SetBoundsWithAnimation(bounds);
335 if (iter == toasts_.rbegin())
336 break;
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>(
355 alignment_ |
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;
363 int base;
365 if (top_down) {
366 if (!last_toast) {
367 base = work_area_.y();
368 if (!first_item_has_no_margin_)
369 base += kToastMarginY;
370 else
371 base -= kNoToastMarginBorderAndShadowOffset;
372 } else {
373 base = toasts_.back()->bounds().bottom() + kToastMarginY;
375 } else {
376 if (!last_toast) {
377 base = work_area_.bottom();
378 if (!first_item_has_no_margin_)
379 base -= kToastMarginY;
380 else
381 base += kNoToastMarginBorderAndShadowOffset;
382 } else {
383 base = toasts_.back()->origin().y() - kToastMarginY;
386 return base;
389 void MessagePopupCollection::OnNotificationAdded(
390 const std::string& notification_id) {
391 DoUpdateIfPossible();
394 void MessagePopupCollection::OnNotificationRemoved(
395 const std::string& notification_id,
396 bool by_user) {
397 // Find a toast.
398 Toasts::const_iterator iter = toasts_.begin();
399 for (; iter != toasts_.end(); ++iter) {
400 if ((*iter)->id() == notification_id)
401 break;
403 if (iter == toasts_.end())
404 return;
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);
425 if (by_user)
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) {
438 // Find a toast.
439 Toasts::const_iterator toast_iter = toasts_.begin();
440 for (; toast_iter != toasts_.end(); ++toast_iter) {
441 if ((*toast_iter)->id() == notification_id)
442 break;
444 if (toast_iter == toasts_.end())
445 return;
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)
459 continue;
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);
469 updated = true;
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.
475 if (!updated)
476 RemoveToast(*toast_iter, /*mark_as_shown=*/true);
478 if (user_is_closing_toasts_by_clicking_)
479 RepositionWidgetsWithTarget();
480 else
481 DoUpdateIfPossible();
484 ToastContentsView* MessagePopupCollection::FindToast(
485 const std::string& notification_id) const {
486 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
487 ++iter) {
488 if ((*iter)->id() == notification_id)
489 return *iter;
491 return NULL;
494 void MessagePopupCollection::IncrementDeferCounter() {
495 defer_counter_++;
498 void MessagePopupCollection::DecrementDeferCounter() {
499 defer_counter_--;
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() {
511 if (!screen_) {
512 gfx::Display display;
513 if (!parent_) {
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();
519 } else {
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)
534 return;
536 RepositionWidgets();
538 if (defer_counter_ > 0)
539 return;
541 // Reposition could create extra space which allows additional widgets.
542 UpdateWidgets();
544 if (defer_counter_ > 0)
545 return;
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)
557 return;
559 work_area_ = work_area;
560 ComputePopupAlignment(work_area, screen_bounds);
561 RepositionWidgets();
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_)
578 return;
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)
585 const {
586 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
587 ++iter) {
588 if ((*iter)->id() == id)
589 return (*iter)->GetWidget();
591 return NULL;
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.";
605 size_t i = 0;
606 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
607 ++iter) {
608 if (i++ == index) {
609 views::Widget* widget = (*iter)->GetWidget();
610 if (widget)
611 return widget->GetWindowBoundsInScreen();
612 break;
615 return gfx::Rect();
618 } // namespace message_center