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/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/toast_contents_view.h" 28 #include "ui/views/background.h" 29 #include "ui/views/layout/fill_layout.h" 30 #include "ui/views/view.h" 31 #include "ui/views/views_delegate.h" 32 #include "ui/views/widget/widget.h" 33 #include "ui/views/widget/widget_delegate.h" 34 35 namespace message_center { 36 namespace { 37 38 // Timeout between the last user-initiated close of the toast and the moment 39 // when normal layout/update of the toast stack continues. If the last toast was 40 // just closed, the timeout is shorter. 41 const int kMouseExitedDeferTimeoutMs = 200; 42 43 // The margin between messages (and between the anchor unless 44 // first_item_has_no_margin was specified). 45 const int kToastMarginY = kMarginBetweenItems; 46 #if defined(OS_CHROMEOS) 47 const int kToastMarginX = 3; 48 #else 49 const int kToastMarginX = kMarginBetweenItems; 50 #endif 51 52 53 // If there should be no margin for the first item, this value needs to be 54 // substracted to flush the message to the shelf (the width of the border + 55 // shadow). 56 const int kNoToastMarginBorderAndShadowOffset = 2; 57 58 } // namespace. 59 60 MessagePopupCollection::MessagePopupCollection(gfx::NativeView parent, 61 MessageCenter* message_center, 62 MessageCenterTray* tray, 63 bool first_item_has_no_margin) 64 : parent_(parent), 65 message_center_(message_center), 66 tray_(tray), 67 display_id_(gfx::Display::kInvalidDisplayID), 68 screen_(NULL), 69 defer_counter_(0), 70 latest_toast_entered_(NULL), 71 user_is_closing_toasts_by_clicking_(false), 72 first_item_has_no_margin_(first_item_has_no_margin), 73 context_menu_controller_(new MessageViewContextMenuController(this)), 74 weak_factory_(this) { 75 DCHECK(message_center_); 76 defer_timer_.reset(new base::OneShotTimer<MessagePopupCollection>); 77 message_center_->AddObserver(this); 78 } 79 80 MessagePopupCollection::~MessagePopupCollection() { 81 weak_factory_.InvalidateWeakPtrs(); 82 83 if (screen_) 84 screen_->RemoveObserver(this); 85 message_center_->RemoveObserver(this); 86 87 CloseAllWidgets(); 88 } 89 90 void MessagePopupCollection::ClickOnNotification( 91 const std::string& notification_id) { 92 message_center_->ClickOnNotification(notification_id); 93 } 94 95 void MessagePopupCollection::RemoveNotification( 96 const std::string& notification_id, 97 bool by_user) { 98 message_center_->RemoveNotification(notification_id, by_user); 99 } 100 101 scoped_ptr<ui::MenuModel> MessagePopupCollection::CreateMenuModel( 102 const NotifierId& notifier_id, 103 const base::string16& display_source) { 104 return tray_->CreateNotificationMenuModel(notifier_id, display_source); 105 } 106 107 bool MessagePopupCollection::HasClickedListener( 108 const std::string& notification_id) { 109 return message_center_->HasClickedListener(notification_id); 110 } 111 112 void MessagePopupCollection::ClickOnNotificationButton( 113 const std::string& notification_id, 114 int button_index) { 115 message_center_->ClickOnNotificationButton(notification_id, button_index); 116 } 117 118 void MessagePopupCollection::MarkAllPopupsShown() { 119 std::set<std::string> closed_ids = CloseAllWidgets(); 120 for (std::set<std::string>::iterator iter = closed_ids.begin(); 121 iter != closed_ids.end(); iter++) { 122 message_center_->MarkSinglePopupAsShown(*iter, false); 123 } 124 } 125 126 void MessagePopupCollection::UpdateWidgets() { 127 NotificationList::PopupNotifications popups = 128 message_center_->GetPopupNotifications(); 129 130 if (popups.empty()) { 131 CloseAllWidgets(); 132 return; 133 } 134 135 bool top_down = alignment_ & POPUP_ALIGNMENT_TOP; 136 int base = GetBaseLine(toasts_.empty() ? NULL : toasts_.back()); 137 138 // Iterate in the reverse order to keep the oldest toasts on screen. Newer 139 // items may be ignored if there are no room to place them. 140 for (NotificationList::PopupNotifications::const_reverse_iterator iter = 141 popups.rbegin(); iter != popups.rend(); ++iter) { 142 if (FindToast((*iter)->id())) 143 continue; 144 145 NotificationView* view = 146 NotificationView::Create(NULL, 147 *(*iter), 148 true); // Create top-level notification. 149 view->set_context_menu_controller(context_menu_controller_.get()); 150 int view_height = ToastContentsView::GetToastSizeForView(view).height(); 151 int height_available = top_down ? work_area_.bottom() - base : base; 152 153 if (height_available - view_height - kToastMarginY < 0) { 154 delete view; 155 break; 156 } 157 158 ToastContentsView* toast = 159 new ToastContentsView((*iter)->id(), weak_factory_.GetWeakPtr()); 160 // There will be no contents already since this is a new ToastContentsView. 161 toast->SetContents(view, /*a11y_feedback_for_updates=*/false); 162 toasts_.push_back(toast); 163 view->set_controller(toast); 164 165 gfx::Size preferred_size = toast->GetPreferredSize(); 166 gfx::Point origin(GetToastOriginX(gfx::Rect(preferred_size)), base); 167 // The toast slides in from the edge of the screen horizontally. 168 if (alignment_ & POPUP_ALIGNMENT_LEFT) 169 origin.set_x(origin.x() - preferred_size.width()); 170 else 171 origin.set_x(origin.x() + preferred_size.width()); 172 if (top_down) 173 origin.set_y(origin.y() + view_height); 174 175 toast->RevealWithAnimation(origin); 176 177 // Shift the base line to be a few pixels above the last added toast or (few 178 // pixels below last added toast if top-aligned). 179 if (top_down) 180 base += view_height + kToastMarginY; 181 else 182 base -= view_height + kToastMarginY; 183 184 if (views::ViewsDelegate::views_delegate) { 185 views::ViewsDelegate::views_delegate->NotifyAccessibilityEvent( 186 toast, ui::AX_EVENT_ALERT); 187 } 188 189 message_center_->DisplayedNotification( 190 (*iter)->id(), message_center::DISPLAY_SOURCE_POPUP); 191 } 192 } 193 194 void MessagePopupCollection::OnMouseEntered(ToastContentsView* toast_entered) { 195 // Sometimes we can get two MouseEntered/MouseExited in a row when animating 196 // toasts. So we need to keep track of which one is the currently active one. 197 latest_toast_entered_ = toast_entered; 198 199 message_center_->PausePopupTimers(); 200 201 if (user_is_closing_toasts_by_clicking_) 202 defer_timer_->Stop(); 203 } 204 205 void MessagePopupCollection::OnMouseExited(ToastContentsView* toast_exited) { 206 // If we're exiting a toast after entering a different toast, then ignore 207 // this mouse event. 208 if (toast_exited != latest_toast_entered_) 209 return; 210 latest_toast_entered_ = NULL; 211 212 if (user_is_closing_toasts_by_clicking_) { 213 defer_timer_->Start( 214 FROM_HERE, 215 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs), 216 this, 217 &MessagePopupCollection::OnDeferTimerExpired); 218 } else { 219 message_center_->RestartPopupTimers(); 220 } 221 } 222 223 std::set<std::string> MessagePopupCollection::CloseAllWidgets() { 224 std::set<std::string> closed_toast_ids; 225 226 while (!toasts_.empty()) { 227 ToastContentsView* toast = toasts_.front(); 228 toasts_.pop_front(); 229 closed_toast_ids.insert(toast->id()); 230 231 OnMouseExited(toast); 232 233 // CloseWithAnimation will cause the toast to forget about |this| so it is 234 // required when we forget a toast. 235 toast->CloseWithAnimation(); 236 } 237 238 return closed_toast_ids; 239 } 240 241 void MessagePopupCollection::ForgetToast(ToastContentsView* toast) { 242 toasts_.remove(toast); 243 OnMouseExited(toast); 244 } 245 246 void MessagePopupCollection::RemoveToast(ToastContentsView* toast, 247 bool mark_as_shown) { 248 ForgetToast(toast); 249 250 toast->CloseWithAnimation(); 251 252 if (mark_as_shown) 253 message_center_->MarkSinglePopupAsShown(toast->id(), false); 254 } 255 256 int MessagePopupCollection::GetToastOriginX(const gfx::Rect& toast_bounds) 257 const { 258 #if defined(OS_CHROMEOS) 259 // In ChromeOS, RTL UI language mirrors the whole desktop layout, so the toast 260 // widgets should be at the bottom-left instead of bottom right. 261 if (base::i18n::IsRTL()) 262 return work_area_.x() + kToastMarginX; 263 #endif 264 if (alignment_ & POPUP_ALIGNMENT_LEFT) 265 return work_area_.x() + kToastMarginX; 266 return work_area_.right() - kToastMarginX - toast_bounds.width(); 267 } 268 269 void MessagePopupCollection::RepositionWidgets() { 270 bool top_down = alignment_ & POPUP_ALIGNMENT_TOP; 271 int base = GetBaseLine(NULL); // We don't want to position relative to last 272 // toast - we want re-position. 273 274 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end();) { 275 Toasts::const_iterator curr = iter++; 276 gfx::Rect bounds((*curr)->bounds()); 277 bounds.set_x(GetToastOriginX(bounds)); 278 bounds.set_y(alignment_ & POPUP_ALIGNMENT_TOP ? base 279 : base - bounds.height()); 280 281 // The notification may scrolls the boundary of the screen due to image 282 // load and such notifications should disappear. Do not call 283 // CloseWithAnimation, we don't want to show the closing animation, and we 284 // don't want to mark such notifications as shown. See crbug.com/233424 285 if ((top_down ? work_area_.bottom() - bounds.bottom() : bounds.y()) >= 0) 286 (*curr)->SetBoundsWithAnimation(bounds); 287 else 288 RemoveToast(*curr, /*mark_as_shown=*/false); 289 290 // Shift the base line to be a few pixels above the last added toast or (few 291 // pixels below last added toast if top-aligned). 292 if (top_down) 293 base += bounds.height() + kToastMarginY; 294 else 295 base -= bounds.height() + kToastMarginY; 296 } 297 } 298 299 void MessagePopupCollection::RepositionWidgetsWithTarget() { 300 if (toasts_.empty()) 301 return; 302 303 bool top_down = alignment_ & POPUP_ALIGNMENT_TOP; 304 305 // Nothing to do if there are no widgets above target if bottom-aligned or no 306 // widgets below target if top-aligned. 307 if (top_down ? toasts_.back()->origin().y() < target_top_edge_ 308 : toasts_.back()->origin().y() > target_top_edge_) 309 return; 310 311 Toasts::reverse_iterator iter = toasts_.rbegin(); 312 for (; iter != toasts_.rend(); ++iter) { 313 // We only reposition widgets above target if bottom-aligned or widgets 314 // below target if top-aligned. 315 if (top_down ? (*iter)->origin().y() < target_top_edge_ 316 : (*iter)->origin().y() > target_top_edge_) 317 break; 318 } 319 --iter; 320 321 // Slide length is the number of pixels the widgets should move so that their 322 // bottom edge (top-edge if top-aligned) touches the target. 323 int slide_length = std::abs(target_top_edge_ - (*iter)->origin().y()); 324 for (;; --iter) { 325 gfx::Rect bounds((*iter)->bounds()); 326 327 // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned, 328 // shift them downwards by slide_length. 329 if (top_down) 330 bounds.set_y(bounds.y() - slide_length); 331 else 332 bounds.set_y(bounds.y() + slide_length); 333 (*iter)->SetBoundsWithAnimation(bounds); 334 335 if (iter == toasts_.rbegin()) 336 break; 337 } 338 } 339 340 void MessagePopupCollection::ComputePopupAlignment(gfx::Rect work_area, 341 gfx::Rect screen_bounds) { 342 // If the taskbar is at the top, render notifications top down. Some platforms 343 // like Gnome can have taskbars at top and bottom. In this case it's more 344 // likely that the systray is on the top one. 345 alignment_ = work_area.y() > screen_bounds.y() ? POPUP_ALIGNMENT_TOP 346 : POPUP_ALIGNMENT_BOTTOM; 347 348 // If the taskbar is on the left show the notifications on the left. Otherwise 349 // show it on right since it's very likely that the systray is on the right if 350 // the taskbar is on the top or bottom. 351 // Since on some platforms like Ubuntu Unity there's also a launcher along 352 // with a taskbar (panel), we need to check that there is really nothing at 353 // the top before concluding that the taskbar is at the left. 354 alignment_ = static_cast<PopupAlignment>( 355 alignment_ | 356 ((work_area.x() > screen_bounds.x() && work_area.y() == screen_bounds.y()) 357 ? POPUP_ALIGNMENT_LEFT 358 : POPUP_ALIGNMENT_RIGHT)); 359 } 360 361 int MessagePopupCollection::GetBaseLine(ToastContentsView* last_toast) const { 362 bool top_down = alignment_ & POPUP_ALIGNMENT_TOP; 363 int base; 364 365 if (top_down) { 366 if (!last_toast) { 367 base = work_area_.y(); 368 if (!first_item_has_no_margin_) 369 base += kToastMarginY; 370 else 371 base -= kNoToastMarginBorderAndShadowOffset; 372 } else { 373 base = toasts_.back()->bounds().bottom() + kToastMarginY; 374 } 375 } else { 376 if (!last_toast) { 377 base = work_area_.bottom(); 378 if (!first_item_has_no_margin_) 379 base -= kToastMarginY; 380 else 381 base += kNoToastMarginBorderAndShadowOffset; 382 } else { 383 base = toasts_.back()->origin().y() - kToastMarginY; 384 } 385 } 386 return base; 387 } 388 389 void MessagePopupCollection::OnNotificationAdded( 390 const std::string& notification_id) { 391 DoUpdateIfPossible(); 392 } 393 394 void MessagePopupCollection::OnNotificationRemoved( 395 const std::string& notification_id, 396 bool by_user) { 397 // Find a toast. 398 Toasts::const_iterator iter = toasts_.begin(); 399 for (; iter != toasts_.end(); ++iter) { 400 if ((*iter)->id() == notification_id) 401 break; 402 } 403 if (iter == toasts_.end()) 404 return; 405 406 target_top_edge_ = (*iter)->bounds().y(); 407 if (by_user && !user_is_closing_toasts_by_clicking_) { 408 // [Re] start a timeout after which the toasts re-position to their 409 // normal locations after tracking the mouse pointer for easy deletion. 410 // This provides a period of time when toasts are easy to remove because 411 // they re-position themselves to have Close button right under the mouse 412 // pointer. If the user continue to remove the toasts, the delay is reset. 413 // Once user stopped removing the toasts, the toasts re-populate/rearrange 414 // after the specified delay. 415 user_is_closing_toasts_by_clicking_ = true; 416 IncrementDeferCounter(); 417 } 418 419 // CloseWithAnimation ultimately causes a call to RemoveToast, which calls 420 // OnMouseExited. This means that |user_is_closing_toasts_by_clicking_| must 421 // have been set before this call, otherwise it will remain true even after 422 // the toast is closed, since the defer timer won't be started. 423 RemoveToast(*iter, /*mark_as_shown=*/true); 424 425 if (by_user) 426 RepositionWidgetsWithTarget(); 427 } 428 429 void MessagePopupCollection::OnDeferTimerExpired() { 430 user_is_closing_toasts_by_clicking_ = false; 431 DecrementDeferCounter(); 432 433 message_center_->RestartPopupTimers(); 434 } 435 436 void MessagePopupCollection::OnNotificationUpdated( 437 const std::string& notification_id) { 438 // Find a toast. 439 Toasts::const_iterator toast_iter = toasts_.begin(); 440 for (; toast_iter != toasts_.end(); ++toast_iter) { 441 if ((*toast_iter)->id() == notification_id) 442 break; 443 } 444 if (toast_iter == toasts_.end()) 445 return; 446 447 NotificationList::PopupNotifications notifications = 448 message_center_->GetPopupNotifications(); 449 bool updated = false; 450 451 for (NotificationList::PopupNotifications::iterator iter = 452 notifications.begin(); iter != notifications.end(); ++iter) { 453 Notification* notification = *iter; 454 DCHECK(notification); 455 ToastContentsView* toast_contents_view = *toast_iter; 456 DCHECK(toast_contents_view); 457 458 if (notification->id() != notification_id) 459 continue; 460 461 const RichNotificationData& optional_fields = 462 notification->rich_notification_data(); 463 bool a11y_feedback_for_updates = 464 optional_fields.should_make_spoken_feedback_for_popup_updates; 465 466 toast_contents_view->UpdateContents(*notification, 467 a11y_feedback_for_updates); 468 469 updated = true; 470 } 471 472 // OnNotificationUpdated() can be called when a notification is excluded from 473 // the popup notification list but still remains in the full notification 474 // list. In that case the widget for the notification has to be closed here. 475 if (!updated) 476 RemoveToast(*toast_iter, /*mark_as_shown=*/true); 477 478 if (user_is_closing_toasts_by_clicking_) 479 RepositionWidgetsWithTarget(); 480 else 481 DoUpdateIfPossible(); 482 } 483 484 ToastContentsView* MessagePopupCollection::FindToast( 485 const std::string& notification_id) const { 486 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end(); 487 ++iter) { 488 if ((*iter)->id() == notification_id) 489 return *iter; 490 } 491 return NULL; 492 } 493 494 void MessagePopupCollection::IncrementDeferCounter() { 495 defer_counter_++; 496 } 497 498 void MessagePopupCollection::DecrementDeferCounter() { 499 defer_counter_--; 500 DCHECK(defer_counter_ >= 0); 501 DoUpdateIfPossible(); 502 } 503 504 // This is the main sequencer of tasks. It does a step, then waits for 505 // all started transitions to play out before doing the next step. 506 // First, remove all expired toasts. 507 // Then, reposition widgets (the reposition on close happens before all 508 // deferred tasks are even able to run) 509 // Then, see if there is vacant space for new toasts. 510 void MessagePopupCollection::DoUpdateIfPossible() { 511 if (!screen_) { 512 gfx::Display display; 513 if (!parent_) { 514 // On Win+Aura, we don't have a parent since the popups currently show up 515 // on the Windows desktop, not in the Aura/Ash desktop. This code will 516 // display the popups on the primary display. 517 screen_ = gfx::Screen::GetNativeScreen(); 518 display = screen_->GetPrimaryDisplay(); 519 } else { 520 screen_ = gfx::Screen::GetScreenFor(parent_); 521 display = screen_->GetDisplayNearestWindow(parent_); 522 } 523 screen_->AddObserver(this); 524 525 display_id_ = display.id(); 526 // |work_area_| can be set already and it should not be overwritten here. 527 if (work_area_.IsEmpty()) { 528 work_area_ = display.work_area(); 529 ComputePopupAlignment(work_area_, display.bounds()); 530 } 531 } 532 533 if (defer_counter_ > 0) 534 return; 535 536 RepositionWidgets(); 537 538 if (defer_counter_ > 0) 539 return; 540 541 // Reposition could create extra space which allows additional widgets. 542 UpdateWidgets(); 543 544 if (defer_counter_ > 0) 545 return; 546 547 // Test support. Quit the test run loop when no more updates are deferred, 548 // meaining th echeck for updates did not cause anything to change so no new 549 // transition animations were started. 550 if (run_loop_for_test_.get()) 551 run_loop_for_test_->Quit(); 552 } 553 554 void MessagePopupCollection::SetDisplayInfo(const gfx::Rect& work_area, 555 const gfx::Rect& screen_bounds) { 556 if (work_area_ == work_area) 557 return; 558 559 work_area_ = work_area; 560 ComputePopupAlignment(work_area, screen_bounds); 561 RepositionWidgets(); 562 } 563 564 void MessagePopupCollection::OnDisplayAdded(const gfx::Display& new_display) { 565 } 566 567 void MessagePopupCollection::OnDisplayRemoved(const gfx::Display& old_display) { 568 if (display_id_ == old_display.id() && !parent_) { 569 gfx::Display display = gfx::Screen::GetNativeScreen()->GetPrimaryDisplay(); 570 display_id_ = display.id(); 571 SetDisplayInfo(display.work_area(), display.bounds()); 572 } 573 } 574 575 void MessagePopupCollection::OnDisplayMetricsChanged( 576 const gfx::Display& display, uint32_t metrics) { 577 if (display.id() != display_id_) 578 return; 579 580 if (metrics & DISPLAY_METRIC_BOUNDS || metrics & DISPLAY_METRIC_WORK_AREA) 581 SetDisplayInfo(display.work_area(), display.bounds()); 582 } 583 584 views::Widget* MessagePopupCollection::GetWidgetForTest(const std::string& id) 585 const { 586 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end(); 587 ++iter) { 588 if ((*iter)->id() == id) 589 return (*iter)->GetWidget(); 590 } 591 return NULL; 592 } 593 594 void MessagePopupCollection::CreateRunLoopForTest() { 595 run_loop_for_test_.reset(new base::RunLoop()); 596 } 597 598 void MessagePopupCollection::WaitForTest() { 599 run_loop_for_test_->Run(); 600 run_loop_for_test_.reset(); 601 } 602 603 gfx::Rect MessagePopupCollection::GetToastRectAt(size_t index) const { 604 DCHECK(defer_counter_ == 0) << "Fetching the bounds with animations active."; 605 size_t i = 0; 606 for (Toasts::const_iterator iter = toasts_.begin(); iter != toasts_.end(); 607 ++iter) { 608 if (i++ == index) { 609 views::Widget* widget = (*iter)->GetWidget(); 610 if (widget) 611 return widget->GetWindowBoundsInScreen(); 612 break; 613 } 614 } 615 return gfx::Rect(); 616 } 617 618 } // namespace message_center 619