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