Add ICU message format support
[chromium-blink-merge.git] / ui / message_center / views / message_popup_collection.cc
bloba021eb740dae6f7bd8143fc56f0a0d6dc0153e3b
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 NotificationList::PopupNotifications popups =
116 message_center_->GetPopupNotifications();
118 if (popups.empty()) {
119 CloseAllWidgets();
120 return;
123 bool top_down = alignment_delegate_->IsTopDown();
124 int base = GetBaseLine(toasts_.empty() ? NULL : toasts_.back());
126 // Iterate in the reverse order to keep the oldest toasts on screen. Newer
127 // items may be ignored if there are no room to place them.
128 for (NotificationList::PopupNotifications::const_reverse_iterator iter =
129 popups.rbegin(); iter != popups.rend(); ++iter) {
130 if (FindToast((*iter)->id()))
131 continue;
133 NotificationView* view =
134 NotificationView::Create(NULL,
135 *(*iter),
136 true); // Create top-level notification.
137 view->set_context_menu_controller(context_menu_controller_.get());
138 int view_height = ToastContentsView::GetToastSizeForView(view).height();
139 int height_available =
140 top_down ? alignment_delegate_->GetWorkAreaBottom() - base : base;
142 if (height_available - view_height - kToastMarginY < 0) {
143 delete view;
144 break;
147 ToastContentsView* toast =
148 new ToastContentsView((*iter)->id(), weak_factory_.GetWeakPtr());
149 // There will be no contents already since this is a new ToastContentsView.
150 toast->SetContents(view, /*a11y_feedback_for_updates=*/false);
151 toasts_.push_back(toast);
152 view->set_controller(toast);
154 gfx::Size preferred_size = toast->GetPreferredSize();
155 gfx::Point origin(
156 alignment_delegate_->GetToastOriginX(gfx::Rect(preferred_size)), base);
157 // The toast slides in from the edge of the screen horizontally.
158 if (alignment_delegate_->IsFromLeft())
159 origin.set_x(origin.x() - preferred_size.width());
160 else
161 origin.set_x(origin.x() + preferred_size.width());
162 if (top_down)
163 origin.set_y(origin.y() + view_height);
165 toast->RevealWithAnimation(origin);
167 // Shift the base line to be a few pixels above the last added toast or (few
168 // pixels below last added toast if top-aligned).
169 if (top_down)
170 base += view_height + kToastMarginY;
171 else
172 base -= view_height + kToastMarginY;
174 if (views::ViewsDelegate::GetInstance()) {
175 views::ViewsDelegate::GetInstance()->NotifyAccessibilityEvent(
176 toast, ui::AX_EVENT_ALERT);
179 message_center_->DisplayedNotification(
180 (*iter)->id(), message_center::DISPLAY_SOURCE_POPUP);
184 void MessagePopupCollection::OnMouseEntered(ToastContentsView* toast_entered) {
185 // Sometimes we can get two MouseEntered/MouseExited in a row when animating
186 // toasts. So we need to keep track of which one is the currently active one.
187 latest_toast_entered_ = toast_entered;
189 message_center_->PausePopupTimers();
191 if (user_is_closing_toasts_by_clicking_)
192 defer_timer_->Stop();
195 void MessagePopupCollection::OnMouseExited(ToastContentsView* toast_exited) {
196 // If we're exiting a toast after entering a different toast, then ignore
197 // this mouse event.
198 if (toast_exited != latest_toast_entered_)
199 return;
200 latest_toast_entered_ = NULL;
202 if (user_is_closing_toasts_by_clicking_) {
203 defer_timer_->Start(
204 FROM_HERE,
205 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs),
206 this,
207 &MessagePopupCollection::OnDeferTimerExpired);
208 } else {
209 message_center_->RestartPopupTimers();
213 std::set<std::string> MessagePopupCollection::CloseAllWidgets() {
214 std::set<std::string> closed_toast_ids;
216 while (!toasts_.empty()) {
217 ToastContentsView* toast = toasts_.front();
218 toasts_.pop_front();
219 closed_toast_ids.insert(toast->id());
221 OnMouseExited(toast);
223 // CloseWithAnimation will cause the toast to forget about |this| so it is
224 // required when we forget a toast.
225 toast->CloseWithAnimation();
228 return closed_toast_ids;
231 void MessagePopupCollection::ForgetToast(ToastContentsView* toast) {
232 toasts_.remove(toast);
233 OnMouseExited(toast);
236 void MessagePopupCollection::RemoveToast(ToastContentsView* toast,
237 bool mark_as_shown) {
238 ForgetToast(toast);
240 toast->CloseWithAnimation();
242 if (mark_as_shown)
243 message_center_->MarkSinglePopupAsShown(toast->id(), false);
246 void MessagePopupCollection::RepositionWidgets() {
247 bool top_down = alignment_delegate_->IsTopDown();
248 int base = GetBaseLine(NULL); // We don't want to position relative to last
249 // toast - we want re-position.
251 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();) {
252 Toasts::const_iterator curr = iter++;
253 gfx::Rect bounds((*curr)->bounds());
254 bounds.set_x(alignment_delegate_->GetToastOriginX(bounds));
255 bounds.set_y(top_down ? base : base - bounds.height());
257 // The notification may scrolls the boundary of the screen due to image
258 // load and such notifications should disappear. Do not call
259 // CloseWithAnimation, we don't want to show the closing animation, and we
260 // don't want to mark such notifications as shown. See crbug.com/233424
261 if ((top_down ? alignment_delegate_->GetWorkAreaBottom() - bounds.bottom()
262 : bounds.y()) >= 0)
263 (*curr)->SetBoundsWithAnimation(bounds);
264 else
265 RemoveToast(*curr, /*mark_as_shown=*/false);
267 // Shift the base line to be a few pixels above the last added toast or (few
268 // pixels below last added toast if top-aligned).
269 if (top_down)
270 base += bounds.height() + kToastMarginY;
271 else
272 base -= bounds.height() + kToastMarginY;
276 void MessagePopupCollection::RepositionWidgetsWithTarget() {
277 if (toasts_.empty())
278 return;
280 bool top_down = alignment_delegate_->IsTopDown();
282 // Nothing to do if there are no widgets above target if bottom-aligned or no
283 // widgets below target if top-aligned.
284 if (top_down ? toasts_.back()->origin().y() < target_top_edge_
285 : toasts_.back()->origin().y() > target_top_edge_)
286 return;
288 Toasts::reverse_iterator iter = toasts_.rbegin();
289 for (; iter != toasts_.rend(); ++iter) {
290 // We only reposition widgets above target if bottom-aligned or widgets
291 // below target if top-aligned.
292 if (top_down ? (*iter)->origin().y() < target_top_edge_
293 : (*iter)->origin().y() > target_top_edge_)
294 break;
296 --iter;
298 // Slide length is the number of pixels the widgets should move so that their
299 // bottom edge (top-edge if top-aligned) touches the target.
300 int slide_length = std::abs(target_top_edge_ - (*iter)->origin().y());
301 for (;; --iter) {
302 gfx::Rect bounds((*iter)->bounds());
304 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
305 // shift them downwards by slide_length.
306 if (top_down)
307 bounds.set_y(bounds.y() - slide_length);
308 else
309 bounds.set_y(bounds.y() + slide_length);
310 (*iter)->SetBoundsWithAnimation(bounds);
312 if (iter == toasts_.rbegin())
313 break;
317 int MessagePopupCollection::GetBaseLine(ToastContentsView* last_toast) const {
318 if (!last_toast) {
319 return alignment_delegate_->GetBaseLine();
320 } else if (alignment_delegate_->IsTopDown()) {
321 return toasts_.back()->bounds().bottom() + kToastMarginY;
322 } else {
323 return toasts_.back()->origin().y() - kToastMarginY;
327 void MessagePopupCollection::OnNotificationAdded(
328 const std::string& notification_id) {
329 DoUpdateIfPossible();
332 void MessagePopupCollection::OnNotificationRemoved(
333 const std::string& notification_id,
334 bool by_user) {
335 // Find a toast.
336 Toasts::const_iterator iter = toasts_.begin();
337 for (; iter != toasts_.end(); ++iter) {
338 if ((*iter)->id() == notification_id)
339 break;
341 if (iter == toasts_.end())
342 return;
344 target_top_edge_ = (*iter)->bounds().y();
345 if (by_user && !user_is_closing_toasts_by_clicking_) {
346 // [Re] start a timeout after which the toasts re-position to their
347 // normal locations after tracking the mouse pointer for easy deletion.
348 // This provides a period of time when toasts are easy to remove because
349 // they re-position themselves to have Close button right under the mouse
350 // pointer. If the user continue to remove the toasts, the delay is reset.
351 // Once user stopped removing the toasts, the toasts re-populate/rearrange
352 // after the specified delay.
353 user_is_closing_toasts_by_clicking_ = true;
354 IncrementDeferCounter();
357 // CloseWithAnimation ultimately causes a call to RemoveToast, which calls
358 // OnMouseExited. This means that |user_is_closing_toasts_by_clicking_| must
359 // have been set before this call, otherwise it will remain true even after
360 // the toast is closed, since the defer timer won't be started.
361 RemoveToast(*iter, /*mark_as_shown=*/true);
363 if (by_user)
364 RepositionWidgetsWithTarget();
367 void MessagePopupCollection::OnDeferTimerExpired() {
368 user_is_closing_toasts_by_clicking_ = false;
369 DecrementDeferCounter();
371 message_center_->RestartPopupTimers();
374 void MessagePopupCollection::OnNotificationUpdated(
375 const std::string& notification_id) {
376 // Find a toast.
377 Toasts::const_iterator toast_iter = toasts_.begin();
378 for (; toast_iter != toasts_.end(); ++toast_iter) {
379 if ((*toast_iter)->id() == notification_id)
380 break;
382 if (toast_iter == toasts_.end())
383 return;
385 NotificationList::PopupNotifications notifications =
386 message_center_->GetPopupNotifications();
387 bool updated = false;
389 for (NotificationList::PopupNotifications::iterator iter =
390 notifications.begin(); iter != notifications.end(); ++iter) {
391 Notification* notification = *iter;
392 DCHECK(notification);
393 ToastContentsView* toast_contents_view = *toast_iter;
394 DCHECK(toast_contents_view);
396 if (notification->id() != notification_id)
397 continue;
399 const RichNotificationData& optional_fields =
400 notification->rich_notification_data();
401 bool a11y_feedback_for_updates =
402 optional_fields.should_make_spoken_feedback_for_popup_updates;
404 toast_contents_view->UpdateContents(*notification,
405 a11y_feedback_for_updates);
407 updated = true;
410 // OnNotificationUpdated() can be called when a notification is excluded from
411 // the popup notification list but still remains in the full notification
412 // list. In that case the widget for the notification has to be closed here.
413 if (!updated)
414 RemoveToast(*toast_iter, /*mark_as_shown=*/true);
416 if (user_is_closing_toasts_by_clicking_)
417 RepositionWidgetsWithTarget();
418 else
419 DoUpdateIfPossible();
422 ToastContentsView* MessagePopupCollection::FindToast(
423 const std::string& notification_id) const {
424 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
425 ++iter) {
426 if ((*iter)->id() == notification_id)
427 return *iter;
429 return NULL;
432 void MessagePopupCollection::IncrementDeferCounter() {
433 defer_counter_++;
436 void MessagePopupCollection::DecrementDeferCounter() {
437 defer_counter_--;
438 DCHECK(defer_counter_ >= 0);
439 DoUpdateIfPossible();
442 // This is the main sequencer of tasks. It does a step, then waits for
443 // all started transitions to play out before doing the next step.
444 // First, remove all expired toasts.
445 // Then, reposition widgets (the reposition on close happens before all
446 // deferred tasks are even able to run)
447 // Then, see if there is vacant space for new toasts.
448 void MessagePopupCollection::DoUpdateIfPossible() {
449 if (defer_counter_ > 0)
450 return;
452 RepositionWidgets();
454 if (defer_counter_ > 0)
455 return;
457 // Reposition could create extra space which allows additional widgets.
458 UpdateWidgets();
460 if (defer_counter_ > 0)
461 return;
463 // Test support. Quit the test run loop when no more updates are deferred,
464 // meaining th echeck for updates did not cause anything to change so no new
465 // transition animations were started.
466 if (run_loop_for_test_.get())
467 run_loop_for_test_->Quit();
470 void MessagePopupCollection::OnDisplayMetricsChanged(
471 const gfx::Display& display) {
472 alignment_delegate_->RecomputeAlignment(display);
475 views::Widget* MessagePopupCollection::GetWidgetForTest(const std::string& id)
476 const {
477 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
478 ++iter) {
479 if ((*iter)->id() == id)
480 return (*iter)->GetWidget();
482 return NULL;
485 void MessagePopupCollection::CreateRunLoopForTest() {
486 run_loop_for_test_.reset(new base::RunLoop());
489 void MessagePopupCollection::WaitForTest() {
490 run_loop_for_test_->Run();
491 run_loop_for_test_.reset();
494 gfx::Rect MessagePopupCollection::GetToastRectAt(size_t index) const {
495 DCHECK(defer_counter_ == 0) << "Fetching the bounds with animations active.";
496 size_t i = 0;
497 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();
498 ++iter) {
499 if (i++ == index) {
500 views::Widget* widget = (*iter)->GetWidget();
501 if (widget)
502 return widget->GetWindowBoundsInScreen();
503 break;
506 return gfx::Rect();
509 } // namespace message_center