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/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" 35 36 namespace message_center { 37 namespace { 38 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; 43 44 // The margin between messages (and between the anchor unless 45 // first_item_has_no_margin was specified). 46 const int kToastMarginY = kMarginBetweenItems; 47 48 } // namespace. 49 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); 68 } 69 70 MessagePopupCollection::~MessagePopupCollection() { 71 weak_factory_.InvalidateWeakPtrs(); 72 73 message_center_->RemoveObserver(this); 74 75 CloseAllWidgets(); 76 } 77 78 void MessagePopupCollection::ClickOnNotification( 79 const std::string& notification_id) { 80 message_center_->ClickOnNotification(notification_id); 81 } 82 83 void MessagePopupCollection::RemoveNotification( 84 const std::string& notification_id, 85 bool by_user) { 86 message_center_->RemoveNotification(notification_id, by_user); 87 } 88 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); 93 } 94 95 bool MessagePopupCollection::HasClickedListener( 96 const std::string& notification_id) { 97 return message_center_->HasClickedListener(notification_id); 98 } 99 100 void MessagePopupCollection::ClickOnNotificationButton( 101 const std::string& notification_id, 102 int button_index) { 103 message_center_->ClickOnNotificationButton(notification_id, button_index); 104 } 105 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); 111 } 112 } 113 114 void MessagePopupCollection::UpdateWidgets() { 115 NotificationList::PopupNotifications popups = 116 message_center_->GetPopupNotifications(); 117 118 if (popups.empty()) { 119 CloseAllWidgets(); 120 return; 121 } 122 123 bool top_down = alignment_delegate_->IsTopDown(); 124 int base = GetBaseLine(toasts_.empty() ? NULL : toasts_.back()); 125 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; 132 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; 141 142 if (height_available - view_height - kToastMarginY < 0) { 143 delete view; 144 break; 145 } 146 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); 153 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); 164 165 toast->RevealWithAnimation(origin); 166 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; 173 174 if (views::ViewsDelegate::views_delegate) { 175 views::ViewsDelegate::views_delegate->NotifyAccessibilityEvent( 176 toast, ui::AX_EVENT_ALERT); 177 } 178 179 message_center_->DisplayedNotification( 180 (*iter)->id(), message_center::DISPLAY_SOURCE_POPUP); 181 } 182 } 183 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; 188 189 message_center_->PausePopupTimers(); 190 191 if (user_is_closing_toasts_by_clicking_) 192 defer_timer_->Stop(); 193 } 194 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; 201 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(); 210 } 211 } 212 213 std::set<std::string> MessagePopupCollection::CloseAllWidgets() { 214 std::set<std::string> closed_toast_ids; 215 216 while (!toasts_.empty()) { 217 ToastContentsView* toast = toasts_.front(); 218 toasts_.pop_front(); 219 closed_toast_ids.insert(toast->id()); 220 221 OnMouseExited(toast); 222 223 // CloseWithAnimation will cause the toast to forget about |this| so it is 224 // required when we forget a toast. 225 toast->CloseWithAnimation(); 226 } 227 228 return closed_toast_ids; 229 } 230 231 void MessagePopupCollection::ForgetToast(ToastContentsView* toast) { 232 toasts_.remove(toast); 233 OnMouseExited(toast); 234 } 235 236 void MessagePopupCollection::RemoveToast(ToastContentsView* toast, 237 bool mark_as_shown) { 238 ForgetToast(toast); 239 240 toast->CloseWithAnimation(); 241 242 if (mark_as_shown) 243 message_center_->MarkSinglePopupAsShown(toast->id(), false); 244 } 245 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. 250 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()); 256 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); 266 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; 273 } 274 } 275 276 void MessagePopupCollection::RepositionWidgetsWithTarget() { 277 if (toasts_.empty()) 278 return; 279 280 bool top_down = alignment_delegate_->IsTopDown(); 281 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; 287 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; 295 } 296 --iter; 297 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()); 303 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); 311 312 if (iter == toasts_.rbegin()) 313 break; 314 } 315 } 316 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; 324 } 325 } 326 327 void MessagePopupCollection::OnNotificationAdded( 328 const std::string& notification_id) { 329 DoUpdateIfPossible(); 330 } 331 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; 340 } 341 if (iter == toasts_.end()) 342 return; 343 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(); 355 } 356 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); 362 363 if (by_user) 364 RepositionWidgetsWithTarget(); 365 } 366 367 void MessagePopupCollection::OnDeferTimerExpired() { 368 user_is_closing_toasts_by_clicking_ = false; 369 DecrementDeferCounter(); 370 371 message_center_->RestartPopupTimers(); 372 } 373 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; 381 } 382 if (toast_iter == toasts_.end()) 383 return; 384 385 NotificationList::PopupNotifications notifications = 386 message_center_->GetPopupNotifications(); 387 bool updated = false; 388 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); 395 396 if (notification->id() != notification_id) 397 continue; 398 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; 403 404 toast_contents_view->UpdateContents(*notification, 405 a11y_feedback_for_updates); 406 407 updated = true; 408 } 409 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); 415 416 if (user_is_closing_toasts_by_clicking_) 417 RepositionWidgetsWithTarget(); 418 else 419 DoUpdateIfPossible(); 420 } 421 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; 428 } 429 return NULL; 430 } 431 432 void MessagePopupCollection::IncrementDeferCounter() { 433 defer_counter_++; 434 } 435 436 void MessagePopupCollection::DecrementDeferCounter() { 437 defer_counter_--; 438 DCHECK(defer_counter_ >= 0); 439 DoUpdateIfPossible(); 440 } 441 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; 451 452 RepositionWidgets(); 453 454 if (defer_counter_ > 0) 455 return; 456 457 // Reposition could create extra space which allows additional widgets. 458 UpdateWidgets(); 459 460 if (defer_counter_ > 0) 461 return; 462 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(); 468 } 469 470 void MessagePopupCollection::OnDisplayMetricsChanged( 471 const gfx::Display& display) { 472 alignment_delegate_->RecomputeAlignment(display); 473 } 474 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(); 481 } 482 return NULL; 483 } 484 485 void MessagePopupCollection::CreateRunLoopForTest() { 486 run_loop_for_test_.reset(new base::RunLoop()); 487 } 488 489 void MessagePopupCollection::WaitForTest() { 490 run_loop_for_test_->Run(); 491 run_loop_for_test_.reset(); 492 } 493 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; 504 } 505 } 506 return gfx::Rect(); 507 } 508 509 } // namespace message_center 510