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/toast_contents_view.h" 6 7 #include "base/bind.h" 8 #include "base/compiler_specific.h" 9 #include "base/memory/scoped_ptr.h" 10 #include "base/memory/weak_ptr.h" 11 #include "base/time/time.h" 12 #include "base/timer/timer.h" 13 #include "ui/base/accessibility/accessible_view_state.h" 14 #include "ui/gfx/animation/animation_delegate.h" 15 #include "ui/gfx/animation/slide_animation.h" 16 #include "ui/gfx/display.h" 17 #include "ui/gfx/screen.h" 18 #include "ui/message_center/message_center_style.h" 19 #include "ui/message_center/notification.h" 20 #include "ui/message_center/views/message_popup_collection.h" 21 #include "ui/message_center/views/message_view.h" 22 #include "ui/views/background.h" 23 #include "ui/views/view.h" 24 #include "ui/views/widget/widget.h" 25 #include "ui/views/widget/widget_delegate.h" 26 27 #if defined(OS_WIN) && defined(USE_ASH) 28 #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" 29 #endif 30 31 namespace message_center { 32 namespace { 33 34 // The width of a toast before animated reveal and after closing. 35 const int kClosedToastWidth = 5; 36 37 // FadeIn/Out look a bit better if they are slightly longer then default slide. 38 const int kFadeInOutDuration = 200; 39 40 } // namespace. 41 42 // static 43 gfx::Size ToastContentsView::GetToastSizeForView(views::View* view) { 44 int width = kNotificationWidth + view->GetInsets().width(); 45 return gfx::Size(width, view->GetHeightForWidth(width)); 46 } 47 48 ToastContentsView::ToastContentsView( 49 const std::string& notification_id, 50 base::WeakPtr<MessagePopupCollection> collection) 51 : collection_(collection), 52 id_(notification_id), 53 is_animating_bounds_(false), 54 is_closing_(false), 55 closing_animation_(NULL) { 56 set_notify_enter_exit_on_child(true); 57 // Sets the transparent background. Then, when the message view is slid out, 58 // the whole toast seems to slide although the actual bound of the widget 59 // remains. This is hacky but easier to keep the consistency. 60 set_background(views::Background::CreateSolidBackground(0, 0, 0, 0)); 61 62 fade_animation_.reset(new gfx::SlideAnimation(this)); 63 fade_animation_->SetSlideDuration(kFadeInOutDuration); 64 65 CreateWidget(collection->parent()); 66 } 67 68 // This is destroyed when the toast window closes. 69 ToastContentsView::~ToastContentsView() { 70 if (collection_) 71 collection_->ForgetToast(this); 72 } 73 74 void ToastContentsView::SetContents(MessageView* view, 75 bool a11y_feedback_for_updates) { 76 bool already_has_contents = child_count() > 0; 77 RemoveAllChildViews(true); 78 AddChildView(view); 79 preferred_size_ = GetToastSizeForView(view); 80 Layout(); 81 82 // If it has the contents already, this invocation means an update of the 83 // popup toast, and the new contents should be read through a11y feature. 84 // The notification type should be ALERT, otherwise the accessibility message 85 // won't be read for this view which returns ROLE_WINDOW. 86 if (already_has_contents && a11y_feedback_for_updates) 87 NotifyAccessibilityEvent(ui::AccessibilityTypes::EVENT_ALERT, false); 88 } 89 90 void ToastContentsView::RevealWithAnimation(gfx::Point origin) { 91 // Place/move the toast widgets. Currently it stacks the widgets from the 92 // right-bottom of the work area. 93 // TODO(mukai): allow to specify the placement policy from outside of this 94 // class. The policy should be specified from preference on Windows, or 95 // the launcher alignment on ChromeOS. 96 origin_ = gfx::Point(origin.x() - preferred_size_.width(), 97 origin.y() - preferred_size_.height()); 98 99 gfx::Rect stable_bounds(origin_, preferred_size_); 100 101 SetBoundsInstantly(GetClosedToastBounds(stable_bounds)); 102 StartFadeIn(); 103 SetBoundsWithAnimation(stable_bounds); 104 } 105 106 void ToastContentsView::CloseWithAnimation() { 107 if (is_closing_) 108 return; 109 is_closing_ = true; 110 StartFadeOut(); 111 } 112 113 void ToastContentsView::SetBoundsInstantly(gfx::Rect new_bounds) { 114 if (new_bounds == bounds()) 115 return; 116 117 origin_ = new_bounds.origin(); 118 if (!GetWidget()) 119 return; 120 GetWidget()->SetBounds(new_bounds); 121 } 122 123 void ToastContentsView::SetBoundsWithAnimation(gfx::Rect new_bounds) { 124 if (new_bounds == bounds()) 125 return; 126 127 origin_ = new_bounds.origin(); 128 if (!GetWidget()) 129 return; 130 131 // This picks up the current bounds, so if there was a previous animation 132 // half-done, the next one will pick up from the current location. 133 // This is the only place that should query current location of the Widget 134 // on screen, the rest should refer to the bounds_. 135 animated_bounds_start_ = GetWidget()->GetWindowBoundsInScreen(); 136 animated_bounds_end_ = new_bounds; 137 138 if (collection_) 139 collection_->IncrementDeferCounter(); 140 141 if (bounds_animation_.get()) 142 bounds_animation_->Stop(); 143 144 bounds_animation_.reset(new gfx::SlideAnimation(this)); 145 bounds_animation_->Show(); 146 } 147 148 void ToastContentsView::StartFadeIn() { 149 // The decrement is done in OnBoundsAnimationEndedOrCancelled callback. 150 if (collection_) 151 collection_->IncrementDeferCounter(); 152 fade_animation_->Stop(); 153 154 GetWidget()->SetOpacity(0); 155 GetWidget()->Show(); 156 fade_animation_->Reset(0); 157 fade_animation_->Show(); 158 } 159 160 void ToastContentsView::StartFadeOut() { 161 // The decrement is done in OnBoundsAnimationEndedOrCancelled callback. 162 if (collection_) 163 collection_->IncrementDeferCounter(); 164 fade_animation_->Stop(); 165 166 closing_animation_ = (is_closing_ ? fade_animation_.get() : NULL); 167 fade_animation_->Reset(1); 168 fade_animation_->Hide(); 169 } 170 171 void ToastContentsView::OnBoundsAnimationEndedOrCancelled( 172 const gfx::Animation* animation) { 173 if (is_closing_ && closing_animation_ == animation && GetWidget()) { 174 views::Widget* widget = GetWidget(); 175 #if defined(USE_AURA) 176 // TODO(dewittj): This is a workaround to prevent a nasty bug where 177 // closing a transparent widget doesn't actually remove the window, 178 // causing entire areas of the screen to become unresponsive to clicks. 179 // See crbug.com/243469 180 widget->Hide(); 181 # if defined(OS_WIN) 182 widget->SetOpacity(0xFF); 183 # endif 184 #endif 185 widget->Close(); 186 } 187 188 // This cannot be called before GetWidget()->Close(). Decrementing defer count 189 // will invoke update, which may invoke another close animation with 190 // incrementing defer counter. Close() after such process will cause a 191 // mismatch between increment/decrement. See crbug.com/238477 192 if (collection_) 193 collection_->DecrementDeferCounter(); 194 } 195 196 // gfx::AnimationDelegate 197 void ToastContentsView::AnimationProgressed(const gfx::Animation* animation) { 198 if (animation == bounds_animation_.get()) { 199 gfx::Rect current(animation->CurrentValueBetween( 200 animated_bounds_start_, animated_bounds_end_)); 201 GetWidget()->SetBounds(current); 202 } else if (animation == fade_animation_.get()) { 203 unsigned char opacity = 204 static_cast<unsigned char>(fade_animation_->GetCurrentValue() * 255); 205 GetWidget()->SetOpacity(opacity); 206 } 207 } 208 209 void ToastContentsView::AnimationEnded(const gfx::Animation* animation) { 210 OnBoundsAnimationEndedOrCancelled(animation); 211 } 212 213 void ToastContentsView::AnimationCanceled( 214 const gfx::Animation* animation) { 215 OnBoundsAnimationEndedOrCancelled(animation); 216 } 217 218 // views::WidgetDelegate 219 views::View* ToastContentsView::GetContentsView() { 220 return this; 221 } 222 223 void ToastContentsView::WindowClosing() { 224 if (!is_closing_ && collection_.get()) 225 collection_->ForgetToast(this); 226 } 227 228 bool ToastContentsView::CanActivate() const { 229 #if defined(OS_WIN) && defined(USE_AURA) 230 return true; 231 #else 232 return false; 233 #endif 234 } 235 236 void ToastContentsView::OnDisplayChanged() { 237 views::Widget* widget = GetWidget(); 238 if (!widget) 239 return; 240 241 gfx::NativeView native_view = widget->GetNativeView(); 242 if (!native_view || !collection_.get()) 243 return; 244 245 collection_->OnDisplayBoundsChanged(gfx::Screen::GetScreenFor( 246 native_view)->GetDisplayNearestWindow(native_view)); 247 } 248 249 void ToastContentsView::OnWorkAreaChanged() { 250 views::Widget* widget = GetWidget(); 251 if (!widget) 252 return; 253 254 gfx::NativeView native_view = widget->GetNativeView(); 255 if (!native_view || !collection_.get()) 256 return; 257 258 collection_->OnDisplayBoundsChanged(gfx::Screen::GetScreenFor( 259 native_view)->GetDisplayNearestWindow(native_view)); 260 } 261 262 // views::View 263 void ToastContentsView::OnMouseEntered(const ui::MouseEvent& event) { 264 if (collection_) 265 collection_->OnMouseEntered(this); 266 } 267 268 void ToastContentsView::OnMouseExited(const ui::MouseEvent& event) { 269 if (collection_) 270 collection_->OnMouseExited(this); 271 } 272 273 void ToastContentsView::Layout() { 274 if (child_count() > 0) { 275 child_at(0)->SetBounds( 276 0, 0, preferred_size_.width(), preferred_size_.height()); 277 } 278 } 279 280 gfx::Size ToastContentsView::GetPreferredSize() { 281 return child_count() ? GetToastSizeForView(child_at(0)) : gfx::Size(); 282 } 283 284 void ToastContentsView::GetAccessibleState(ui::AccessibleViewState* state) { 285 if (child_count() > 0) 286 child_at(0)->GetAccessibleState(state); 287 state->role = ui::AccessibilityTypes::ROLE_WINDOW; 288 } 289 290 void ToastContentsView::ClickOnNotification( 291 const std::string& notification_id) { 292 if (collection_) 293 collection_->ClickOnNotification(notification_id); 294 } 295 296 void ToastContentsView::RemoveNotification( 297 const std::string& notification_id, 298 bool by_user) { 299 if (collection_) 300 collection_->RemoveNotification(notification_id, by_user); 301 } 302 303 void ToastContentsView::DisableNotificationsFromThisSource( 304 const NotifierId& notifier_id) { 305 if (collection_) 306 collection_->DisableNotificationsFromThisSource(notifier_id); 307 } 308 309 void ToastContentsView::ShowNotifierSettingsBubble() { 310 if (collection_) 311 collection_->ShowNotifierSettingsBubble(); 312 } 313 314 bool ToastContentsView::HasClickedListener( 315 const std::string& notification_id) { 316 if (!collection_) 317 return false; 318 return collection_->HasClickedListener(notification_id); 319 } 320 321 void ToastContentsView::ClickOnNotificationButton( 322 const std::string& notification_id, 323 int button_index) { 324 if (collection_) 325 collection_->ClickOnNotificationButton(notification_id, button_index); 326 } 327 328 void ToastContentsView::ExpandNotification( 329 const std::string& notification_id) { 330 if (collection_) 331 collection_->ExpandNotification(notification_id); 332 } 333 334 void ToastContentsView::GroupBodyClicked( 335 const std::string& last_notification_id) { 336 // No group views in popup collection. 337 NOTREACHED(); 338 } 339 340 // When clicked on the "N more" button, perform some reasonable action. 341 // TODO(dimich): find out what the reasonable action could be. 342 void ToastContentsView::ExpandGroup(const NotifierId& notifier_id) { 343 // No group views in popup collection. 344 NOTREACHED(); 345 } 346 347 void ToastContentsView::RemoveGroup(const NotifierId& notifier_id) { 348 // No group views in popup collection. 349 NOTREACHED(); 350 } 351 352 void ToastContentsView::CreateWidget(gfx::NativeView parent) { 353 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); 354 params.keep_on_top = true; 355 if (parent) 356 params.parent = parent; 357 else 358 params.top_level = true; 359 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 360 params.delegate = this; 361 views::Widget* widget = new views::Widget(); 362 widget->set_focus_on_creation(false); 363 364 #if defined(OS_WIN) && defined(USE_ASH) 365 // We want to ensure that this toast always goes to the native desktop, 366 // not the Ash desktop (since there is already another toast contents view 367 // there. 368 if (!params.parent) 369 params.native_widget = new views::DesktopNativeWidgetAura(widget); 370 #endif 371 372 widget->Init(params); 373 } 374 375 gfx::Rect ToastContentsView::GetClosedToastBounds(gfx::Rect bounds) { 376 return gfx::Rect(bounds.x() + bounds.width() - kClosedToastWidth, 377 bounds.y(), 378 kClosedToastWidth, 379 bounds.height()); 380 } 381 382 } // namespace message_center 383