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 gfx::DisplayObserver::DISPLAY_METRIC_BOUNDS | 251 gfx::DisplayObserver::DISPLAY_METRIC_WORK_AREA); 252 } 253 254 void ToastContentsView::OnWorkAreaChanged() { 255 views::Widget* widget = GetWidget(); 256 if (!widget) 257 return; 258 259 gfx::NativeView native_view = widget->GetNativeView(); 260 if (!native_view || !collection_.get()) 261 return; 262 263 collection_->OnDisplayMetricsChanged( 264 Screen::GetScreenFor(native_view)->GetDisplayNearestWindow(native_view), 265 gfx::DisplayObserver::DISPLAY_METRIC_WORK_AREA); 266 } 267 268 // views::View 269 void ToastContentsView::OnMouseEntered(const ui::MouseEvent& event) { 270 if (collection_) 271 collection_->OnMouseEntered(this); 272 } 273 274 void ToastContentsView::OnMouseExited(const ui::MouseEvent& event) { 275 if (collection_) 276 collection_->OnMouseExited(this); 277 } 278 279 void ToastContentsView::Layout() { 280 if (child_count() > 0) { 281 child_at(0)->SetBounds( 282 0, 0, preferred_size_.width(), preferred_size_.height()); 283 } 284 } 285 286 gfx::Size ToastContentsView::GetPreferredSize() const { 287 return child_count() ? GetToastSizeForView(child_at(0)) : gfx::Size(); 288 } 289 290 void ToastContentsView::GetAccessibleState(ui::AXViewState* state) { 291 if (child_count() > 0) 292 child_at(0)->GetAccessibleState(state); 293 state->role = ui::AX_ROLE_WINDOW; 294 } 295 296 void ToastContentsView::ClickOnNotification( 297 const std::string& notification_id) { 298 if (collection_) 299 collection_->ClickOnNotification(notification_id); 300 } 301 302 void ToastContentsView::RemoveNotification( 303 const std::string& notification_id, 304 bool by_user) { 305 if (collection_) 306 collection_->RemoveNotification(notification_id, by_user); 307 } 308 309 scoped_ptr<ui::MenuModel> ToastContentsView::CreateMenuModel( 310 const NotifierId& notifier_id, 311 const base::string16& display_source) { 312 // Should not reach, the context menu should be handled in 313 // MessagePopupCollection. 314 NOTREACHED(); 315 return scoped_ptr<ui::MenuModel>(); 316 } 317 318 bool ToastContentsView::HasClickedListener( 319 const std::string& notification_id) { 320 if (!collection_) 321 return false; 322 return collection_->HasClickedListener(notification_id); 323 } 324 325 void ToastContentsView::ClickOnNotificationButton( 326 const std::string& notification_id, 327 int button_index) { 328 if (collection_) 329 collection_->ClickOnNotificationButton(notification_id, button_index); 330 } 331 332 void ToastContentsView::CreateWidget(gfx::NativeView parent) { 333 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); 334 params.keep_on_top = true; 335 if (parent) 336 params.parent = parent; 337 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 338 params.delegate = this; 339 views::Widget* widget = new views::Widget(); 340 widget->set_focus_on_creation(false); 341 342 #if defined(OS_WIN) 343 // We want to ensure that this toast always goes to the native desktop, 344 // not the Ash desktop (since there is already another toast contents view 345 // there. 346 if (!params.parent) 347 params.native_widget = new views::DesktopNativeWidgetAura(widget); 348 #endif 349 350 widget->Init(params); 351 } 352 353 gfx::Rect ToastContentsView::GetClosedToastBounds(gfx::Rect bounds) { 354 return gfx::Rect(bounds.x() + bounds.width() - kClosedToastWidth, 355 bounds.y(), 356 kClosedToastWidth, 357 bounds.height()); 358 } 359 360 } // namespace message_center 361