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. 4 5 #include "ui/message_center/views/message_popup_collection.h" 6 7 #include <set> 8 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" 32 33 namespace message_center { 34 namespace { 35 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; 40 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 49 50 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; 55 56 } // namespace. 57 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_); 83 } 84 screen->AddObserver(this); 85 86 display_id_ = display.id(); 87 work_area_ = display.work_area(); 88 ComputePopupAlignment(work_area_, display.bounds()); 89 90 // We should not update before work area and popup alignment are computed. 91 DoUpdateIfPossible(); 92 } 93 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(); 100 } 101 102 void MessagePopupCollection::RemoveToast(ToastContentsView* toast) { 103 OnMouseExited(toast); 104 105 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) { 106 if ((*iter) == toast) { 107 toasts_.erase(iter); 108 break; 109 } 110 } 111 } 112 113 void MessagePopupCollection::UpdateWidgets() { 114 NotificationList::PopupNotifications popups = 115 message_center_->GetPopupNotifications(); 116 117 if (popups.empty()) { 118 CloseAllWidgets(); 119 return; 120 } 121 122 bool top_down = alignment_ & POPUP_ALIGNMENT_TOP; 123 int base = GetBaseLine(toasts_.empty() ? NULL : toasts_.back()); 124 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; 131 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; 140 141 if (height_available - view_height - kToastMarginY < 0) { 142 delete view; 143 break; 144 } 145 146 ToastContentsView* toast = new ToastContentsView( 147 *iter, AsWeakPtr(), message_center_); 148 toast->CreateWidget(parent_); 149 toast->SetContents(view); 150 toasts_.push_back(toast); 151 152 gfx::Size preferred_size = toast->GetPreferredSize(); 153 gfx::Point origin( 154 GetToastOriginX(gfx::Rect(preferred_size)) + preferred_size.width(), 155 top_down ? base + view_height : base); 156 toast->RevealWithAnimation(origin); 157 158 // Shift the base line to be a few pixels above the last added toast or (few 159 // pixels below last added toast if top-aligned). 160 if (top_down) 161 base += view_height + kToastMarginY; 162 else 163 base -= view_height + kToastMarginY; 164 165 message_center_->DisplayedNotification((*iter)->id()); 166 if (views::ViewsDelegate::views_delegate) { 167 views::ViewsDelegate::views_delegate->NotifyAccessibilityEvent( 168 toast, ui::AccessibilityTypes::EVENT_ALERT); 169 } 170 } 171 } 172 173 void MessagePopupCollection::OnMouseEntered(ToastContentsView* toast_entered) { 174 // Sometimes we can get two MouseEntered/MouseExited in a row when animating 175 // toasts. So we need to keep track of which one is the currently active one. 176 latest_toast_entered_ = toast_entered; 177 178 message_center_->PausePopupTimers(); 179 180 if (user_is_closing_toasts_by_clicking_) 181 defer_timer_->Stop(); 182 } 183 184 void MessagePopupCollection::OnMouseExited(ToastContentsView* toast_exited) { 185 // If we're exiting a toast after entering a different toast, then ignore 186 // this mouse event. 187 if (toast_exited != latest_toast_entered_) 188 return; 189 latest_toast_entered_ = NULL; 190 191 if (user_is_closing_toasts_by_clicking_) { 192 defer_timer_->Start( 193 FROM_HERE, 194 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs), 195 this, 196 &MessagePopupCollection::OnDeferTimerExpired); 197 } else { 198 message_center_->RestartPopupTimers(); 199 } 200 } 201 202 void MessagePopupCollection::CloseAllWidgets() { 203 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end();) { 204 // the toast can be removed from toasts_ during CloseWithAnimation(). 205 Toasts::iterator curiter = iter++; 206 (*curiter)->CloseWithAnimation(true); 207 } 208 DCHECK(toasts_.empty()); 209 } 210 211 int MessagePopupCollection::GetToastOriginX(const gfx::Rect& toast_bounds) { 212 #if defined(OS_CHROMEOS) 213 // In ChromeOS, RTL UI language mirrors the whole desktop layout, so the toast 214 // widgets should be at the bottom-left instead of bottom right. 215 if (base::i18n::IsRTL()) 216 return work_area_.x() + kToastMarginX; 217 #endif 218 if (alignment_ & POPUP_ALIGNMENT_LEFT) 219 return work_area_.x() + kToastMarginX; 220 return work_area_.right() - kToastMarginX - toast_bounds.width(); 221 } 222 223 void MessagePopupCollection::RepositionWidgets() { 224 bool top_down = alignment_ & POPUP_ALIGNMENT_TOP; 225 int base = GetBaseLine(NULL); // We don't want to position relative to last 226 // toast - we want re-position. 227 228 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end();) { 229 Toasts::iterator curr = iter++; 230 gfx::Rect bounds((*curr)->bounds()); 231 bounds.set_x(GetToastOriginX(bounds)); 232 bounds.set_y(alignment_ & POPUP_ALIGNMENT_TOP ? base 233 : base - bounds.height()); 234 235 // The notification may scrolls the boundary of the screen due to image 236 // load and such notifications should disappear. Do not call 237 // CloseWithAnimation, we don't want to show the closing animation, and we 238 // don't want to mark such notifications as shown. See crbug.com/233424 239 if ((top_down ? work_area_.bottom() - bounds.bottom() : bounds.y()) >= 0) 240 (*curr)->SetBoundsWithAnimation(bounds); 241 else 242 (*curr)->CloseWithAnimation(false); 243 244 // Shift the base line to be a few pixels above the last added toast or (few 245 // pixels below last added toast if top-aligned). 246 if (top_down) 247 base += bounds.height() + kToastMarginY; 248 else 249 base -= bounds.height() + kToastMarginY; 250 } 251 } 252 253 void MessagePopupCollection::RepositionWidgetsWithTarget() { 254 if (toasts_.empty()) 255 return; 256 257 bool top_down = alignment_ & POPUP_ALIGNMENT_TOP; 258 259 // Nothing to do if there are no widgets above target if bottom-aligned or no 260 // widgets below target if top-aligned. 261 if (top_down ? toasts_.back()->origin().y() < target_top_edge_ 262 : toasts_.back()->origin().y() > target_top_edge_) 263 return; 264 265 Toasts::reverse_iterator iter = toasts_.rbegin(); 266 for (; iter != toasts_.rend(); ++iter) { 267 // We only reposition widgets above target if bottom-aligned or widgets 268 // below target if top-aligned. 269 if (top_down ? (*iter)->origin().y() < target_top_edge_ 270 : (*iter)->origin().y() > target_top_edge_) 271 break; 272 } 273 --iter; 274 275 // Slide length is the number of pixels the widgets should move so that their 276 // bottom edge (top-edge if top-aligned) touches the target. 277 int slide_length = std::abs(target_top_edge_ - (*iter)->origin().y()); 278 for (;; --iter) { 279 gfx::Rect bounds((*iter)->bounds()); 280 281 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned, 282 // shift them downwards by slide_length. 283 if (top_down) 284 bounds.set_y(bounds.y() - slide_length); 285 else 286 bounds.set_y(bounds.y() + slide_length); 287 (*iter)->SetBoundsWithAnimation(bounds); 288 289 if (iter == toasts_.rbegin()) 290 break; 291 } 292 } 293 294 void MessagePopupCollection::ComputePopupAlignment(gfx::Rect work_area, 295 gfx::Rect screen_bounds) { 296 // If the taskbar is at the top, render notifications top down. Some platforms 297 // like Gnome can have taskbars at top and bottom. In this case it's more 298 // likely that the systray is on the top one. 299 alignment_ = work_area.y() > screen_bounds.y() ? POPUP_ALIGNMENT_TOP 300 : POPUP_ALIGNMENT_BOTTOM; 301 302 // If the taskbar is on the left show the notifications on the left. Otherwise 303 // show it on right since it's very likely that the systray is on the right if 304 // the taskbar is on the top or bottom. 305 // Since on some platforms like Ubuntu Unity there's also a launcher along 306 // with a taskbar (panel), we need to check that there is really nothing at 307 // the top before concluding that the taskbar is at the left. 308 alignment_ = static_cast<PopupAlignment>( 309 alignment_ | 310 ((work_area.x() > screen_bounds.x() && work_area.y() == screen_bounds.y()) 311 ? POPUP_ALIGNMENT_LEFT 312 : POPUP_ALIGNMENT_RIGHT)); 313 } 314 315 int MessagePopupCollection::GetBaseLine(ToastContentsView* last_toast) { 316 bool top_down = alignment_ & POPUP_ALIGNMENT_TOP; 317 int base; 318 319 if (top_down) { 320 if (!last_toast) { 321 base = work_area_.y(); 322 if (!first_item_has_no_margin_) 323 base += kToastMarginY; 324 else 325 base -= kNoToastMarginBorderAndShadowOffset; 326 } else { 327 base = toasts_.back()->bounds().bottom() + kToastMarginY; 328 } 329 } else { 330 if (!last_toast) { 331 base = work_area_.bottom(); 332 if (!first_item_has_no_margin_) 333 base -= kToastMarginY; 334 else 335 base += kNoToastMarginBorderAndShadowOffset; 336 } else { 337 base = toasts_.back()->origin().y() - kToastMarginY; 338 } 339 } 340 return base; 341 } 342 343 void MessagePopupCollection::OnNotificationAdded( 344 const std::string& notification_id) { 345 DoUpdateIfPossible(); 346 } 347 348 void MessagePopupCollection::OnNotificationRemoved( 349 const std::string& notification_id, 350 bool by_user) { 351 // Find a toast. 352 Toasts::iterator iter = toasts_.begin(); 353 for (; iter != toasts_.end(); ++iter) { 354 if ((*iter)->id() == notification_id) 355 break; 356 } 357 if (iter == toasts_.end()) 358 return; 359 360 target_top_edge_ = (*iter)->bounds().y(); 361 (*iter)->CloseWithAnimation(true); 362 if (by_user) { 363 RepositionWidgetsWithTarget(); 364 // [Re] start a timeout after which the toasts re-position to their 365 // normal locations after tracking the mouse pointer for easy deletion. 366 // This provides a period of time when toasts are easy to remove because 367 // they re-position themselves to have Close button right under the mouse 368 // pointer. If the user continue to remove the toasts, the delay is reset. 369 // Once user stopped removing the toasts, the toasts re-populate/rearrange 370 // after the specified delay. 371 if (!user_is_closing_toasts_by_clicking_) { 372 user_is_closing_toasts_by_clicking_ = true; 373 IncrementDeferCounter(); 374 } 375 } 376 } 377 378 void MessagePopupCollection::OnDeferTimerExpired() { 379 user_is_closing_toasts_by_clicking_ = false; 380 DecrementDeferCounter(); 381 382 message_center_->RestartPopupTimers(); 383 } 384 385 void MessagePopupCollection::OnNotificationUpdated( 386 const std::string& notification_id) { 387 // Find a toast. 388 Toasts::iterator toast_iter = toasts_.begin(); 389 for (; toast_iter != toasts_.end(); ++toast_iter) { 390 if ((*toast_iter)->id() == notification_id) 391 break; 392 } 393 if (toast_iter == toasts_.end()) 394 return; 395 396 NotificationList::PopupNotifications notifications = 397 message_center_->GetPopupNotifications(); 398 bool updated = false; 399 400 for (NotificationList::PopupNotifications::iterator iter = 401 notifications.begin(); iter != notifications.end(); ++iter) { 402 if ((*iter)->id() != notification_id) 403 continue; 404 405 MessageView* view = 406 NotificationView::Create(*(*iter), 407 message_center_, 408 tray_, 409 true, // Create expanded. 410 true); // Create top-level notification. 411 (*toast_iter)->SetContents(view); 412 updated = true; 413 } 414 415 // OnNotificationUpdated() can be called when a notification is excluded from 416 // the popup notification list but still remains in the full notification 417 // list. In that case the widget for the notification has to be closed here. 418 if (!updated) 419 (*toast_iter)->CloseWithAnimation(true); 420 421 if (user_is_closing_toasts_by_clicking_) 422 RepositionWidgetsWithTarget(); 423 else 424 DoUpdateIfPossible(); 425 } 426 427 ToastContentsView* MessagePopupCollection::FindToast( 428 const std::string& notification_id) { 429 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) { 430 if ((*iter)->id() == notification_id) 431 return *iter; 432 } 433 return NULL; 434 } 435 436 void MessagePopupCollection::IncrementDeferCounter() { 437 defer_counter_++; 438 } 439 440 void MessagePopupCollection::DecrementDeferCounter() { 441 defer_counter_--; 442 DCHECK(defer_counter_ >= 0); 443 DoUpdateIfPossible(); 444 } 445 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; 455 456 RepositionWidgets(); 457 458 if (defer_counter_ > 0) 459 return; 460 461 // Reposition could create extra space which allows additional widgets. 462 UpdateWidgets(); 463 464 if (defer_counter_ > 0) 465 return; 466 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(); 472 } 473 474 void MessagePopupCollection::SetDisplayInfo(const gfx::Rect& work_area, 475 const gfx::Rect& screen_bounds) { 476 if (work_area_ == work_area) 477 return; 478 479 work_area_ = work_area; 480 ComputePopupAlignment(work_area, screen_bounds); 481 RepositionWidgets(); 482 } 483 484 void MessagePopupCollection::OnDisplayBoundsChanged( 485 const gfx::Display& display) { 486 if (display.id() != display_id_) 487 return; 488 489 SetDisplayInfo(display.work_area(), display.bounds()); 490 } 491 492 void MessagePopupCollection::OnDisplayAdded(const gfx::Display& new_display) { 493 } 494 495 void MessagePopupCollection::OnDisplayRemoved(const gfx::Display& old_display) { 496 } 497 498 views::Widget* MessagePopupCollection::GetWidgetForTest(const std::string& id) { 499 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) { 500 if ((*iter)->id() == id) 501 return (*iter)->GetWidget(); 502 } 503 return NULL; 504 } 505 506 void MessagePopupCollection::RunLoopForTest() { 507 run_loop_for_test_.reset(new base::RunLoop()); 508 run_loop_for_test_->Run(); 509 run_loop_for_test_.reset(); 510 } 511 512 gfx::Rect MessagePopupCollection::GetToastRectAt(size_t index) { 513 DCHECK(defer_counter_ == 0) << "Fetching the bounds with animations active."; 514 size_t i = 0; 515 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) { 516 if (i++ == index) { 517 views::Widget* widget = (*iter)->GetWidget(); 518 if (widget) 519 return widget->GetWindowBoundsInScreen(); 520 break; 521 } 522 } 523 return gfx::Rect(); 524 } 525 526 } // namespace message_center 527