message_popup_collection.cc revision 558790d6acca3451cf3a6b497803a5f07d0bec58
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 33namespace message_center { 34namespace { 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. 39const int kMouseExitedDeferTimeoutMs = 200; 40 41// The margin between messages (and between the anchor unless 42// first_item_has_no_margin was specified). 43const int kToastMarginY = kMarginBetweenItems; 44#if defined(OS_CHROMEOS) 45const int kToastMarginX = 3; 46#else 47const 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). 54const int kNoToastMarginBorderAndShadowOffset = 2; 55 56} // namespace. 57 58MessagePopupCollection::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 DoUpdateIfPossible(); 72 message_center_->AddObserver(this); 73 gfx::Screen* screen = NULL; 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 gfx::Display display = screen->GetPrimaryDisplay(); 80 display_id_ = display.id(); 81 work_area_ = display.work_area(); 82 } else { 83 screen = gfx::Screen::GetScreenFor(parent_); 84 gfx::Display display = screen->GetDisplayNearestWindow(parent_); 85 display_id_ = display.id(); 86 work_area_ = display.work_area(); 87 } 88 screen->AddObserver(this); 89} 90 91MessagePopupCollection::~MessagePopupCollection() { 92 gfx::Screen* screen = parent_ ? 93 gfx::Screen::GetScreenFor(parent_) : gfx::Screen::GetNativeScreen(); 94 screen->RemoveObserver(this); 95 message_center_->RemoveObserver(this); 96 CloseAllWidgets(); 97} 98 99void MessagePopupCollection::RemoveToast(ToastContentsView* toast) { 100 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) { 101 if ((*iter) == toast) { 102 toasts_.erase(iter); 103 break; 104 } 105 } 106} 107 108void MessagePopupCollection::UpdateWidgets() { 109 NotificationList::PopupNotifications popups = 110 message_center_->GetPopupNotifications(); 111 112 if (popups.empty()) { 113 CloseAllWidgets(); 114 return; 115 } 116 117 int bottom = toasts_.empty() ? 118 work_area_.bottom() : toasts_.back()->origin().y(); 119 120 if (!first_item_has_no_margin_) 121 bottom -= kToastMarginY; 122 else 123 bottom += kNoToastMarginBorderAndShadowOffset; 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 if (bottom - view_height - kToastMarginY < 0) { 140 delete view; 141 break; 142 } 143 144 ToastContentsView* toast = new ToastContentsView( 145 *iter, AsWeakPtr(), message_center_); 146 toast->CreateWidget(parent_); 147 toast->SetContents(view); 148 toasts_.push_back(toast); 149 150 gfx::Size preferred_size = toast->GetPreferredSize(); 151 gfx::Point origin( 152 GetToastOriginX(gfx::Rect(preferred_size)) + preferred_size.width(), 153 bottom); 154 toast->RevealWithAnimation(origin); 155 bottom -= view_height + kToastMarginY; 156 157 message_center_->DisplayedNotification((*iter)->id()); 158 if (views::ViewsDelegate::views_delegate) { 159 views::ViewsDelegate::views_delegate->NotifyAccessibilityEvent( 160 toast, ui::AccessibilityTypes::EVENT_ALERT); 161 } 162 } 163} 164 165void MessagePopupCollection::OnMouseEntered(ToastContentsView* toast_entered) { 166 // Sometimes we can get two MouseEntered/MouseExited in a row when animating 167 // toasts. So we need to keep track of which one is the currently active one. 168 latest_toast_entered_ = toast_entered; 169 170 message_center_->PausePopupTimers(); 171 172 if (user_is_closing_toasts_by_clicking_) 173 defer_timer_->Stop(); 174} 175 176void MessagePopupCollection::OnMouseExited(ToastContentsView* toast_exited) { 177 // If we're exiting a toast after entering a different toast, then ignore 178 // this mouse event. 179 if (toast_exited != latest_toast_entered_) 180 return; 181 latest_toast_entered_ = NULL; 182 183 if (user_is_closing_toasts_by_clicking_) { 184 defer_timer_->Start( 185 FROM_HERE, 186 base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs), 187 this, 188 &MessagePopupCollection::OnDeferTimerExpired); 189 } else { 190 message_center_->RestartPopupTimers(); 191 } 192} 193 194void MessagePopupCollection::CloseAllWidgets() { 195 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end();) { 196 // the toast can be removed from toasts_ during CloseWithAnimation(). 197 Toasts::iterator curiter = iter++; 198 (*curiter)->CloseWithAnimation(true); 199 } 200 DCHECK(toasts_.empty()); 201} 202 203int MessagePopupCollection::GetToastOriginX(const gfx::Rect& toast_bounds) { 204#if defined(OS_CHROMEOS) 205 // In ChromeOS, RTL UI language mirrors the whole desktop layout, so the toast 206 // widgets should be at the bottom-left instead of bottom right. 207 if (base::i18n::IsRTL()) 208 return work_area_.x() + kToastMarginX; 209#endif 210 return work_area_.right() - kToastMarginX - toast_bounds.width(); 211} 212 213void MessagePopupCollection::RepositionWidgets() { 214 int bottom = work_area_.bottom(); 215 if (!first_item_has_no_margin_) 216 bottom -= kToastMarginY; 217 else 218 bottom += kNoToastMarginBorderAndShadowOffset; 219 220 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end();) { 221 Toasts::iterator curr = iter++; 222 gfx::Rect bounds((*curr)->bounds()); 223 bounds.set_x(GetToastOriginX(bounds)); 224 bounds.set_y(bottom - bounds.height()); 225 // The notification may scrolls the top boundary of the screen due to image 226 // load and such notifications should disappear. Do not call 227 // CloseWithAnimation, we don't want to show the closing animation, and we 228 // don't want to mark such notifications as shown. See crbug.com/233424 229 if (bounds.y() >= 0) 230 (*curr)->SetBoundsWithAnimation(bounds); 231 else 232 (*curr)->CloseWithAnimation(false); 233 bottom -= bounds.height() + kToastMarginY; 234 } 235} 236 237void MessagePopupCollection::RepositionWidgetsWithTarget() { 238 if (toasts_.empty()) 239 return; 240 241 // No widgets above. 242 if (toasts_.back()->origin().y() > target_top_edge_) 243 return; 244 245 Toasts::reverse_iterator iter = toasts_.rbegin(); 246 for (; iter != toasts_.rend(); ++iter) { 247 if ((*iter)->origin().y() > target_top_edge_) 248 break; 249 } 250 --iter; 251 int slide_length = target_top_edge_ - (*iter)->origin().y(); 252 for (; ; --iter) { 253 gfx::Rect bounds((*iter)->bounds()); 254 bounds.set_y(bounds.y() + slide_length); 255 (*iter)->SetBoundsWithAnimation(bounds); 256 257 if (iter == toasts_.rbegin()) 258 break; 259 } 260} 261 262void MessagePopupCollection::OnNotificationAdded( 263 const std::string& notification_id) { 264 DoUpdateIfPossible(); 265} 266 267void MessagePopupCollection::OnNotificationRemoved( 268 const std::string& notification_id, 269 bool by_user) { 270 // Find a toast. 271 Toasts::iterator iter = toasts_.begin(); 272 for (; iter != toasts_.end(); ++iter) { 273 if ((*iter)->id() == notification_id) 274 break; 275 } 276 if (iter == toasts_.end()) 277 return; 278 279 target_top_edge_ = (*iter)->bounds().y(); 280 (*iter)->CloseWithAnimation(true); 281 if (by_user) { 282 RepositionWidgetsWithTarget(); 283 // [Re] start a timeout after which the toasts re-position to their 284 // normal locations after tracking the mouse pointer for easy deletion. 285 // This provides a period of time when toasts are easy to remove because 286 // they re-position themselves to have Close button right under the mouse 287 // pointer. If the user continue to remove the toasts, the delay is reset. 288 // Once user stopped removing the toasts, the toasts re-populate/rearrange 289 // after the specified delay. 290 if (!user_is_closing_toasts_by_clicking_) { 291 user_is_closing_toasts_by_clicking_ = true; 292 IncrementDeferCounter(); 293 } 294 } 295} 296 297void MessagePopupCollection::OnDeferTimerExpired() { 298 user_is_closing_toasts_by_clicking_ = false; 299 DecrementDeferCounter(); 300 301 message_center_->RestartPopupTimers(); 302} 303 304void MessagePopupCollection::OnNotificationUpdated( 305 const std::string& notification_id) { 306 // Find a toast. 307 Toasts::iterator toast_iter = toasts_.begin(); 308 for (; toast_iter != toasts_.end(); ++toast_iter) { 309 if ((*toast_iter)->id() == notification_id) 310 break; 311 } 312 if (toast_iter == toasts_.end()) 313 return; 314 315 NotificationList::PopupNotifications notifications = 316 message_center_->GetPopupNotifications(); 317 bool updated = false; 318 319 for (NotificationList::PopupNotifications::iterator iter = 320 notifications.begin(); iter != notifications.end(); ++iter) { 321 if ((*iter)->id() != notification_id) 322 continue; 323 324 MessageView* view = 325 NotificationView::Create(*(*iter), 326 message_center_, 327 tray_, 328 true, // Create expanded. 329 true); // Create top-level notification. 330 (*toast_iter)->SetContents(view); 331 updated = true; 332 } 333 334 // OnNotificationUpdated() can be called when a notification is excluded from 335 // the popup notification list but still remains in the full notification 336 // list. In that case the widget for the notification has to be closed here. 337 if (!updated) 338 (*toast_iter)->CloseWithAnimation(true); 339 340 if (user_is_closing_toasts_by_clicking_) 341 RepositionWidgetsWithTarget(); 342 else 343 DoUpdateIfPossible(); 344} 345 346ToastContentsView* MessagePopupCollection::FindToast( 347 const std::string& notification_id) { 348 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) { 349 if ((*iter)->id() == notification_id) 350 return *iter; 351 } 352 return NULL; 353} 354 355void MessagePopupCollection::IncrementDeferCounter() { 356 defer_counter_++; 357} 358 359void MessagePopupCollection::DecrementDeferCounter() { 360 defer_counter_--; 361 DCHECK(defer_counter_ >= 0); 362 DoUpdateIfPossible(); 363} 364 365// This is the main sequencer of tasks. It does a step, then waits for 366// all started transitions to play out before doing the next step. 367// First, remove all expired toasts. 368// Then, reposition widgets (the reposition on close happens before all 369// deferred tasks are even able to run) 370// Then, see if there is vacant space for new toasts. 371void MessagePopupCollection::DoUpdateIfPossible() { 372 if (defer_counter_ > 0) 373 return; 374 375 RepositionWidgets(); 376 377 if (defer_counter_ > 0) 378 return; 379 380 // Reposition could create extra space which allows additional widgets. 381 UpdateWidgets(); 382 383 if (defer_counter_ > 0) 384 return; 385 386 // Test support. Quit the test run loop when no more updates are deferred, 387 // meaining th echeck for updates did not cause anything to change so no new 388 // transition animations were started. 389 if (run_loop_for_test_.get()) 390 run_loop_for_test_->Quit(); 391} 392 393void MessagePopupCollection::SetWorkArea(const gfx::Rect& work_area) { 394 if (work_area_ == work_area) 395 return; 396 397 work_area_ = work_area; 398 RepositionWidgets(); 399} 400 401void MessagePopupCollection::OnDisplayBoundsChanged( 402 const gfx::Display& display) { 403 if (display.id() != display_id_) 404 return; 405 406 SetWorkArea(display.work_area()); 407} 408 409void MessagePopupCollection::OnDisplayAdded(const gfx::Display& new_display) { 410} 411 412void MessagePopupCollection::OnDisplayRemoved(const gfx::Display& old_display) { 413} 414 415views::Widget* MessagePopupCollection::GetWidgetForTest(const std::string& id) { 416 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) { 417 if ((*iter)->id() == id) 418 return (*iter)->GetWidget(); 419 } 420 return NULL; 421} 422 423void MessagePopupCollection::RunLoopForTest() { 424 run_loop_for_test_.reset(new base::RunLoop()); 425 run_loop_for_test_->Run(); 426 run_loop_for_test_.reset(); 427} 428 429gfx::Rect MessagePopupCollection::GetToastRectAt(size_t index) { 430 DCHECK(defer_counter_ == 0) << "Fetching the bounds with animations active."; 431 size_t i = 0; 432 for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) { 433 if (i++ == index) { 434 views::Widget* widget = (*iter)->GetWidget(); 435 if (widget) 436 return widget->GetWindowBoundsInScreen(); 437 break; 438 } 439 } 440 return gfx::Rect(); 441} 442 443} // namespace message_center 444