gpu: Tweak Android WebGL test expectations
[chromium-blink-merge.git] / ui / message_center / views / message_popup_collection.cc
blobf08c986894bed6764cf6d0445e46c33a32b90ecd
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/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 {
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 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) {
104 if ((*iter) == toast) {
105 toasts_.erase(iter);
106 break;
111 void MessagePopupCollection::UpdateWidgets() {
112 NotificationList::PopupNotifications popups =
113 message_center_->GetPopupNotifications();
115 if (popups.empty()) {
116 CloseAllWidgets();
117 return;
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()))
128 continue;
130 MessageView* view =
131 NotificationView::Create(*(*iter),
132 message_center_,
133 tray_,
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) {
140 delete view;
141 break;
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();
151 gfx::Point origin(
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).
158 if (top_down)
159 base += view_height + kToastMarginY;
160 else
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
184 // this mouse event.
185 if (toast_exited != latest_toast_entered_)
186 return;
187 latest_toast_entered_ = NULL;
189 if (user_is_closing_toasts_by_clicking_) {
190 defer_timer_->Start(
191 FROM_HERE,
192 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs),
193 this,
194 &MessagePopupCollection::OnDeferTimerExpired);
195 } else {
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;
215 #endif
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);
239 else
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).
244 if (top_down)
245 base += bounds.height() + kToastMarginY;
246 else
247 base -= bounds.height() + kToastMarginY;
251 void MessagePopupCollection::RepositionWidgetsWithTarget() {
252 if (toasts_.empty())
253 return;
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_)
261 return;
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_)
269 break;
271 --iter;
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());
276 for (;; --iter) {
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.
281 if (top_down)
282 bounds.set_y(bounds.y() - slide_length);
283 else
284 bounds.set_y(bounds.y() + slide_length);
285 (*iter)->SetBoundsWithAnimation(bounds);
287 if (iter == toasts_.rbegin())
288 break;
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>(
307 alignment_ |
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;
315 int base;
317 if (top_down) {
318 if (!last_toast) {
319 base = work_area_.y();
320 if (!first_item_has_no_margin_)
321 base += kToastMarginY;
322 else
323 base -= kNoToastMarginBorderAndShadowOffset;
324 } else {
325 base = toasts_.back()->bounds().bottom() + kToastMarginY;
327 } else {
328 if (!last_toast) {
329 base = work_area_.bottom();
330 if (!first_item_has_no_margin_)
331 base -= kToastMarginY;
332 else
333 base += kNoToastMarginBorderAndShadowOffset;
334 } else {
335 base = toasts_.back()->origin().y() - kToastMarginY;
338 return base;
341 void MessagePopupCollection::OnNotificationAdded(
342 const std::string& notification_id) {
343 DoUpdateIfPossible();
346 void MessagePopupCollection::OnNotificationRemoved(
347 const std::string& notification_id,
348 bool by_user) {
349 // Find a toast.
350 Toasts::iterator iter = toasts_.begin();
351 for (; iter != toasts_.end(); ++iter) {
352 if ((*iter)->id() == notification_id)
353 break;
355 if (iter == toasts_.end())
356 return;
358 target_top_edge_ = (*iter)->bounds().y();
359 (*iter)->CloseWithAnimation(true);
360 if (by_user) {
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) {
385 // Find a toast.
386 Toasts::iterator toast_iter = toasts_.begin();
387 for (; toast_iter != toasts_.end(); ++toast_iter) {
388 if ((*toast_iter)->id() == notification_id)
389 break;
391 if (toast_iter == toasts_.end())
392 return;
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)
401 continue;
403 MessageView* view =
404 NotificationView::Create(*(*iter),
405 message_center_,
406 tray_,
407 true, // Create expanded.
408 true); // Create top-level notification.
409 (*toast_iter)->SetContents(view);
410 updated = true;
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.
416 if (!updated)
417 (*toast_iter)->CloseWithAnimation(true);
419 if (user_is_closing_toasts_by_clicking_)
420 RepositionWidgetsWithTarget();
421 else
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)
429 return *iter;
431 return NULL;
434 void MessagePopupCollection::IncrementDeferCounter() {
435 defer_counter_++;
438 void MessagePopupCollection::DecrementDeferCounter() {
439 defer_counter_--;
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)
452 return;
454 RepositionWidgets();
456 if (defer_counter_ > 0)
457 return;
459 // Reposition could create extra space which allows additional widgets.
460 UpdateWidgets();
462 if (defer_counter_ > 0)
463 return;
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)
475 return;
477 work_area_ = work_area;
478 ComputePopupAlignment(work_area, screen_bounds);
479 RepositionWidgets();
482 void MessagePopupCollection::OnDisplayBoundsChanged(
483 const gfx::Display& display) {
484 if (display.id() != display_id_)
485 return;
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();
501 return NULL;
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.";
512 size_t i = 0;
513 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) {
514 if (i++ == index) {
515 views::Widget* widget = (*iter)->GetWidget();
516 if (widget)
517 return widget->GetWindowBoundsInScreen();
518 break;
521 return gfx::Rect();
524 } // namespace message_center