Correct blacklist entry message
[chromium-blink-merge.git] / ui / message_center / views / message_popup_collection.cc
blob5355ad6962b5ceae2a5bff83d259546708d13cef
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/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 {
34 namespace {
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;
46 #else
47 const int kToastMarginX = kMarginBetweenItems;
48 #endif
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 +
53 // shadow).
54 const int kNoToastMarginBorderAndShadowOffset = 2;
56 } // namespace.
58 MessagePopupCollection::MessagePopupCollection(gfx::NativeView parent,
59 MessageCenter* message_center,
60 MessageCenterTray* tray,
61 bool first_item_has_no_margin)
62 : parent_(parent),
63 message_center_(message_center),
64 tray_(tray),
65 defer_counter_(0),
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;
73 gfx::Display display;
74 if (!parent_) {
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();
80 } else {
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.
91 DoUpdateIfPossible();
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);
99 CloseAllWidgets();
102 void MessagePopupCollection::RemoveToast(ToastContentsView* toast) {
103 OnMouseExited(toast);
105 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) {
106 if ((*iter) == toast) {
107 toasts_.erase(iter);
108 break;
113 void MessagePopupCollection::UpdateWidgets() {
114 NotificationList::PopupNotifications popups =
115 message_center_->GetPopupNotifications();
117 if (popups.empty()) {
118 CloseAllWidgets();
119 return;
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()))
130 continue;
132 MessageView* view =
133 NotificationView::Create(*(*iter),
134 message_center_,
135 tray_,
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) {
142 delete view;
143 break;
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());
157 else
158 origin.set_x(origin.x() + preferred_size.width());
159 if (top_down)
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).
165 if (top_down)
166 base += view_height + kToastMarginY;
167 else
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
191 // this mouse event.
192 if (toast_exited != latest_toast_entered_)
193 return;
194 latest_toast_entered_ = NULL;
196 if (user_is_closing_toasts_by_clicking_) {
197 defer_timer_->Start(
198 FROM_HERE,
199 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs),
200 this,
201 &MessagePopupCollection::OnDeferTimerExpired);
202 } else {
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;
222 #endif
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);
246 else
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).
251 if (top_down)
252 base += bounds.height() + kToastMarginY;
253 else
254 base -= bounds.height() + kToastMarginY;
258 void MessagePopupCollection::RepositionWidgetsWithTarget() {
259 if (toasts_.empty())
260 return;
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_)
268 return;
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_)
276 break;
278 --iter;
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());
283 for (;; --iter) {
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.
288 if (top_down)
289 bounds.set_y(bounds.y() - slide_length);
290 else
291 bounds.set_y(bounds.y() + slide_length);
292 (*iter)->SetBoundsWithAnimation(bounds);
294 if (iter == toasts_.rbegin())
295 break;
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>(
314 alignment_ |
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;
322 int base;
324 if (top_down) {
325 if (!last_toast) {
326 base = work_area_.y();
327 if (!first_item_has_no_margin_)
328 base += kToastMarginY;
329 else
330 base -= kNoToastMarginBorderAndShadowOffset;
331 } else {
332 base = toasts_.back()->bounds().bottom() + kToastMarginY;
334 } else {
335 if (!last_toast) {
336 base = work_area_.bottom();
337 if (!first_item_has_no_margin_)
338 base -= kToastMarginY;
339 else
340 base += kNoToastMarginBorderAndShadowOffset;
341 } else {
342 base = toasts_.back()->origin().y() - kToastMarginY;
345 return base;
348 void MessagePopupCollection::OnNotificationAdded(
349 const std::string& notification_id) {
350 DoUpdateIfPossible();
353 void MessagePopupCollection::OnNotificationRemoved(
354 const std::string& notification_id,
355 bool by_user) {
356 // Find a toast.
357 Toasts::iterator iter = toasts_.begin();
358 for (; iter != toasts_.end(); ++iter) {
359 if ((*iter)->id() == notification_id)
360 break;
362 if (iter == toasts_.end())
363 return;
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);
384 if (by_user)
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) {
397 // Find a toast.
398 Toasts::iterator toast_iter = toasts_.begin();
399 for (; toast_iter != toasts_.end(); ++toast_iter) {
400 if ((*toast_iter)->id() == notification_id)
401 break;
403 if (toast_iter == toasts_.end())
404 return;
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)
413 continue;
415 MessageView* view =
416 NotificationView::Create(*(*iter),
417 message_center_,
418 tray_,
419 true, // Create expanded.
420 true); // Create top-level notification.
421 (*toast_iter)->SetContents(view);
422 updated = true;
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.
428 if (!updated)
429 (*toast_iter)->CloseWithAnimation(true);
431 if (user_is_closing_toasts_by_clicking_)
432 RepositionWidgetsWithTarget();
433 else
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)
441 return *iter;
443 return NULL;
446 void MessagePopupCollection::IncrementDeferCounter() {
447 defer_counter_++;
450 void MessagePopupCollection::DecrementDeferCounter() {
451 defer_counter_--;
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)
464 return;
466 RepositionWidgets();
468 if (defer_counter_ > 0)
469 return;
471 // Reposition could create extra space which allows additional widgets.
472 UpdateWidgets();
474 if (defer_counter_ > 0)
475 return;
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)
487 return;
489 work_area_ = work_area;
490 ComputePopupAlignment(work_area, screen_bounds);
491 RepositionWidgets();
494 void MessagePopupCollection::OnDisplayBoundsChanged(
495 const gfx::Display& display) {
496 if (display.id() != display_id_)
497 return;
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();
513 return NULL;
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.";
524 size_t i = 0;
525 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) {
526 if (i++ == index) {
527 views::Widget* widget = (*iter)->GetWidget();
528 if (widget)
529 return widget->GetWindowBoundsInScreen();
530 break;
533 return gfx::Rect();
536 } // namespace message_center