Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / message_center / views / message_popup_collection.cc
bloba4130606d378b147269089320b888b55f143fcbd
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/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 {
37 namespace {
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;
48 } // namespace.
50 MessagePopupCollection::MessagePopupCollection(
51 gfx::NativeView parent,
52 MessageCenter* message_center,
53 MessageCenterTray* tray,
54 PopupAlignmentDelegate* alignment_delegate)
55 : parent_(parent),
56 message_center_(message_center),
57 tray_(tray),
58 alignment_delegate_(alignment_delegate),
59 defer_counter_(0),
60 latest_toast_entered_(NULL),
61 user_is_closing_toasts_by_clicking_(false),
62 context_menu_controller_(new MessageViewContextMenuController(this)),
63 weak_factory_(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);
75 CloseAllWidgets();
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,
85 bool by_user) {
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,
102 int button_index) {
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 if (message_center_->IsMessageCenterVisible()) {
116 DCHECK_EQ(0u, message_center_->GetPopupNotifications().size());
117 return;
120 NotificationList::PopupNotifications popups =
121 message_center_->GetPopupNotifications();
122 if (popups.empty()) {
123 CloseAllWidgets();
124 return;
127 bool top_down = alignment_delegate_->IsTopDown();
128 int base = GetBaseLine(toasts_.empty() ? NULL : toasts_.back());
130 // Iterate in the reverse order to keep the oldest toasts on screen. Newer
131 // items may be ignored if there are no room to place them.
132 for (NotificationList::PopupNotifications::const_reverse_iterator iter =
133 popups.rbegin(); iter != popups.rend(); ++iter) {
134 if (FindToast((*iter)->id()))
135 continue;
137 NotificationView* view =
138 NotificationView::Create(NULL,
139 *(*iter),
140 true); // Create top-level notification.
141 view->set_context_menu_controller(context_menu_controller_.get());
142 int view_height = ToastContentsView::GetToastSizeForView(view).height();
143 int height_available =
144 top_down ? alignment_delegate_->GetWorkAreaBottom() - base : base;
146 if (height_available - view_height - kToastMarginY < 0) {
147 delete view;
148 break;
151 ToastContentsView* toast =
152 new ToastContentsView((*iter)->id(), weak_factory_.GetWeakPtr());
153 // There will be no contents already since this is a new ToastContentsView.
154 toast->SetContents(view, /*a11y_feedback_for_updates=*/false);
155 toasts_.push_back(toast);
156 view->set_controller(toast);
158 gfx::Size preferred_size = toast->GetPreferredSize();
159 gfx::Point origin(
160 alignment_delegate_->GetToastOriginX(gfx::Rect(preferred_size)), base);
161 // The toast slides in from the edge of the screen horizontally.
162 if (alignment_delegate_->IsFromLeft())
163 origin.set_x(origin.x() - preferred_size.width());
164 else
165 origin.set_x(origin.x() + preferred_size.width());
166 if (top_down)
167 origin.set_y(origin.y() + view_height);
169 toast->RevealWithAnimation(origin);
171 // Shift the base line to be a few pixels above the last added toast or (few
172 // pixels below last added toast if top-aligned).
173 if (top_down)
174 base += view_height + kToastMarginY;
175 else
176 base -= view_height + kToastMarginY;
178 if (views::ViewsDelegate::GetInstance()) {
179 views::ViewsDelegate::GetInstance()->NotifyAccessibilityEvent(
180 toast, ui::AX_EVENT_ALERT);
183 message_center_->DisplayedNotification(
184 (*iter)->id(), message_center::DISPLAY_SOURCE_POPUP);
188 void MessagePopupCollection::OnMouseEntered(ToastContentsView* toast_entered) {
189 // Sometimes we can get two MouseEntered/MouseExited in a row when animating
190 // toasts. So we need to keep track of which one is the currently active one.
191 latest_toast_entered_ = toast_entered;
193 message_center_->PausePopupTimers();
195 if (user_is_closing_toasts_by_clicking_)
196 defer_timer_->Stop();
199 void MessagePopupCollection::OnMouseExited(ToastContentsView* toast_exited) {
200 // If we're exiting a toast after entering a different toast, then ignore
201 // this mouse event.
202 if (toast_exited != latest_toast_entered_)
203 return;
204 latest_toast_entered_ = NULL;
206 if (user_is_closing_toasts_by_clicking_) {
207 defer_timer_->Start(
208 FROM_HERE,
209 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs),
210 this,
211 &MessagePopupCollection::OnDeferTimerExpired);
212 } else {
213 message_center_->RestartPopupTimers();
217 std::set<std::string> MessagePopupCollection::CloseAllWidgets() {
218 std::set<std::string> closed_toast_ids;
220 while (!toasts_.empty()) {
221 ToastContentsView* toast = toasts_.front();
222 toasts_.pop_front();
223 closed_toast_ids.insert(toast->id());
225 OnMouseExited(toast);
227 // CloseWithAnimation will cause the toast to forget about |this| so it is
228 // required when we forget a toast.
229 toast->CloseWithAnimation();
232 return closed_toast_ids;
235 void MessagePopupCollection::ForgetToast(ToastContentsView* toast) {
236 toasts_.remove(toast);
237 OnMouseExited(toast);
240 void MessagePopupCollection::RemoveToast(ToastContentsView* toast,
241 bool mark_as_shown) {
242 ForgetToast(toast);
244 toast->CloseWithAnimation();
246 if (mark_as_shown)
247 message_center_->MarkSinglePopupAsShown(toast->id(), false);
250 void MessagePopupCollection::RepositionWidgets() {
251 bool top_down = alignment_delegate_->IsTopDown();
252 int base = GetBaseLine(NULL); // We don't want to position relative to last
253 // toast - we want re-position.
255 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();) {
256 Toasts::const_iterator curr = iter++;
257 gfx::Rect bounds((*curr)->bounds());
258 bounds.set_x(alignment_delegate_->GetToastOriginX(bounds));
259 bounds.set_y(top_down ? base : base - bounds.height());
261 // The notification may scrolls the boundary of the screen due to image
262 // load and such notifications should disappear. Do not call
263 // CloseWithAnimation, we don't want to show the closing animation, and we
264 // don't want to mark such notifications as shown. See crbug.com/233424
265 if ((top_down ? alignment_delegate_->GetWorkAreaBottom() - bounds.bottom()
266 : bounds.y()) >= 0)
267 (*curr)->SetBoundsWithAnimation(bounds);
268 else
269 RemoveToast(*curr, /*mark_as_shown=*/false);
271 // Shift the base line to be a few pixels above the last added toast or (few
272 // pixels below last added toast if top-aligned).
273 if (top_down)
274 base += bounds.height() + kToastMarginY;
275 else
276 base -= bounds.height() + kToastMarginY;
280 void MessagePopupCollection::RepositionWidgetsWithTarget() {
281 if (toasts_.empty())
282 return;
284 bool top_down = alignment_delegate_->IsTopDown();
286 // Nothing to do if there are no widgets above target if bottom-aligned or no
287 // widgets below target if top-aligned.
288 if (top_down ? toasts_.back()->origin().y() < target_top_edge_
289 : toasts_.back()->origin().y() > target_top_edge_)
290 return;
292 Toasts::reverse_iterator iter = toasts_.rbegin();
293 for (; iter != toasts_.rend(); ++iter) {
294 // We only reposition widgets above target if bottom-aligned or widgets
295 // below target if top-aligned.
296 if (top_down ? (*iter)->origin().y() < target_top_edge_
297 : (*iter)->origin().y() > target_top_edge_)
298 break;
300 --iter;
302 // Slide length is the number of pixels the widgets should move so that their
303 // bottom edge (top-edge if top-aligned) touches the target.
304 int slide_length = std::abs(target_top_edge_ - (*iter)->origin().y());
305 for (;; --iter) {
306 gfx::Rect bounds((*iter)->bounds());
308 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
309 // shift them downwards by slide_length.
310 if (top_down)
311 bounds.set_y(bounds.y() - slide_length);
312 else
313 bounds.set_y(bounds.y() + slide_length);
314 (*iter)->SetBoundsWithAnimation(bounds);
316 if (iter == toasts_.rbegin())
317 break;
321 int MessagePopupCollection::GetBaseLine(ToastContentsView* last_toast) const {
322 if (!last_toast) {
323 return alignment_delegate_->GetBaseLine();
324 } else if (alignment_delegate_->IsTopDown()) {
325 return toasts_.back()->bounds().bottom() + kToastMarginY;
326 } else {
327 return toasts_.back()->origin().y() - kToastMarginY;
331 void MessagePopupCollection::OnNotificationAdded(
332 const std::string& notification_id) {
333 DoUpdateIfPossible();
336 void MessagePopupCollection::OnNotificationRemoved(
337 const std::string& notification_id,
338 bool by_user) {
339 // Find a toast.
340 Toasts::const_iterator iter = toasts_.begin();
341 for (; iter != toasts_.end(); ++iter) {
342 if ((*iter)->id() == notification_id)
343 break;
345 if (iter == toasts_.end())
346 return;
348 target_top_edge_ = (*iter)->bounds().y();
349 if (by_user && !user_is_closing_toasts_by_clicking_) {
350 // [Re] start a timeout after which the toasts re-position to their
351 // normal locations after tracking the mouse pointer for easy deletion.
352 // This provides a period of time when toasts are easy to remove because
353 // they re-position themselves to have Close button right under the mouse
354 // pointer. If the user continue to remove the toasts, the delay is reset.
355 // Once user stopped removing the toasts, the toasts re-populate/rearrange
356 // after the specified delay.
357 user_is_closing_toasts_by_clicking_ = true;
358 IncrementDeferCounter();
361 // CloseWithAnimation ultimately causes a call to RemoveToast, which calls
362 // OnMouseExited. This means that |user_is_closing_toasts_by_clicking_| must
363 // have been set before this call, otherwise it will remain true even after
364 // the toast is closed, since the defer timer won't be started.
365 RemoveToast(*iter, /*mark_as_shown=*/true);
367 if (by_user)
368 RepositionWidgetsWithTarget();
371 void MessagePopupCollection::OnDeferTimerExpired() {
372 user_is_closing_toasts_by_clicking_ = false;
373 DecrementDeferCounter();
375 message_center_->RestartPopupTimers();
378 void MessagePopupCollection::OnNotificationUpdated(
379 const std::string& notification_id) {
380 // Find a toast.
381 Toasts::const_iterator toast_iter = toasts_.begin();
382 for (; toast_iter != toasts_.end(); ++toast_iter) {
383 if ((*toast_iter)->id() == notification_id)
384 break;
386 if (toast_iter == toasts_.end())
387 return;
389 NotificationList::PopupNotifications notifications =
390 message_center_->GetPopupNotifications();
391 bool updated = false;
393 for (NotificationList::PopupNotifications::iterator iter =
394 notifications.begin(); iter != notifications.end(); ++iter) {
395 Notification* notification = *iter;
396 DCHECK(notification);
397 ToastContentsView* toast_contents_view = *toast_iter;
398 DCHECK(toast_contents_view);
400 if (notification->id() != notification_id)
401 continue;
403 const RichNotificationData& optional_fields =
404 notification->rich_notification_data();
405 bool a11y_feedback_for_updates =
406 optional_fields.should_make_spoken_feedback_for_popup_updates;
408 toast_contents_view->UpdateContents(*notification,
409 a11y_feedback_for_updates);
411 updated = true;
414 // OnNotificationUpdated() can be called when a notification is excluded from
415 // the popup notification list but still remains in the full notification
416 // list. In that case the widget for the notification has to be closed here.
417 if (!updated)
418 RemoveToast(*toast_iter, /*mark_as_shown=*/true);
420 if (user_is_closing_toasts_by_clicking_)
421 RepositionWidgetsWithTarget();
422 else
423 DoUpdateIfPossible();
426 ToastContentsView* MessagePopupCollection::FindToast(
427 const std::string& notification_id) const {
428 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
429 ++iter) {
430 if ((*iter)->id() == notification_id)
431 return *iter;
433 return NULL;
436 void MessagePopupCollection::IncrementDeferCounter() {
437 defer_counter_++;
440 void MessagePopupCollection::DecrementDeferCounter() {
441 defer_counter_--;
442 DCHECK(defer_counter_ >= 0);
443 DoUpdateIfPossible();
446 // This is the main sequencer of tasks. It does a step, then waits for
447 // all started transitions to play out before doing the next step.
448 // First, remove all expired toasts.
449 // Then, reposition widgets (the reposition on close happens before all
450 // deferred tasks are even able to run)
451 // Then, see if there is vacant space for new toasts.
452 void MessagePopupCollection::DoUpdateIfPossible() {
453 if (defer_counter_ > 0)
454 return;
456 RepositionWidgets();
458 if (defer_counter_ > 0)
459 return;
461 // Reposition could create extra space which allows additional widgets.
462 UpdateWidgets();
464 if (defer_counter_ > 0)
465 return;
467 // Test support. Quit the test run loop when no more updates are deferred,
468 // meaining th echeck for updates did not cause anything to change so no new
469 // transition animations were started.
470 if (run_loop_for_test_.get())
471 run_loop_for_test_->Quit();
474 void MessagePopupCollection::OnDisplayMetricsChanged(
475 const gfx::Display& display) {
476 alignment_delegate_->RecomputeAlignment(display);
479 views::Widget* MessagePopupCollection::GetWidgetForTest(const std::string& id)
480 const {
481 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
482 ++iter) {
483 if ((*iter)->id() == id)
484 return (*iter)->GetWidget();
486 return NULL;
489 void MessagePopupCollection::CreateRunLoopForTest() {
490 run_loop_for_test_.reset(new base::RunLoop());
493 void MessagePopupCollection::WaitForTest() {
494 run_loop_for_test_->Run();
495 run_loop_for_test_.reset();
498 gfx::Rect MessagePopupCollection::GetToastRectAt(size_t index) const {
499 DCHECK(defer_counter_ == 0) << "Fetching the bounds with animations active.";
500 size_t i = 0;
501 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
502 ++iter) {
503 if (i++ == index) {
504 views::Widget* widget = (*iter)->GetWidget();
505 if (widget)
506 return widget->GetWindowBoundsInScreen();
507 break;
510 return gfx::Rect();
513 } // namespace message_center