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_center_view.h" 6 7 #include <list> 8 #include <map> 9 10 #include "base/memory/weak_ptr.h" 11 #include "base/message_loop/message_loop.h" 12 #include "base/stl_util.h" 13 #include "grit/ui_resources.h" 14 #include "grit/ui_strings.h" 15 #include "ui/base/l10n/l10n_util.h" 16 #include "ui/base/resource/resource_bundle.h" 17 #include "ui/gfx/animation/multi_animation.h" 18 #include "ui/gfx/animation/slide_animation.h" 19 #include "ui/gfx/canvas.h" 20 #include "ui/gfx/insets.h" 21 #include "ui/gfx/rect.h" 22 #include "ui/gfx/size.h" 23 #include "ui/message_center/message_center.h" 24 #include "ui/message_center/message_center_style.h" 25 #include "ui/message_center/message_center_tray.h" 26 #include "ui/message_center/message_center_types.h" 27 #include "ui/message_center/message_center_util.h" 28 #include "ui/message_center/views/group_view.h" 29 #include "ui/message_center/views/message_center_button_bar.h" 30 #include "ui/message_center/views/message_view.h" 31 #include "ui/message_center/views/notification_view.h" 32 #include "ui/message_center/views/notifier_settings_view.h" 33 #include "ui/views/animation/bounds_animator.h" 34 #include "ui/views/animation/bounds_animator_observer.h" 35 #include "ui/views/background.h" 36 #include "ui/views/border.h" 37 #include "ui/views/controls/button/button.h" 38 #include "ui/views/controls/label.h" 39 #include "ui/views/controls/scroll_view.h" 40 #include "ui/views/controls/scrollbar/overlay_scroll_bar.h" 41 #include "ui/views/layout/box_layout.h" 42 #include "ui/views/layout/fill_layout.h" 43 #include "ui/views/widget/widget.h" 44 45 namespace message_center { 46 47 namespace { 48 49 const SkColor kNoNotificationsTextColor = SkColorSetRGB(0xb4, 0xb4, 0xb4); 50 #if defined(OS_LINUX) && defined(OS_CHROMEOS) 51 const SkColor kTransparentColor = SkColorSetARGB(0, 0, 0, 0); 52 #endif 53 const int kAnimateClearingNextNotificationDelayMS = 40; 54 const int kMinScrollViewHeight = 100; 55 56 const int kDefaultAnimationDurationMs = 120; 57 const int kDefaultFrameRateHz = 60; 58 59 const int kMaxNotificationCountFromSingleDisplaySource = 1; 60 61 } // namespace 62 63 // BoundedScrollView /////////////////////////////////////////////////////////// 64 65 // A custom scroll view whose height has a minimum and maximum value and whose 66 // scroll bar disappears when not needed. 67 class BoundedScrollView : public views::ScrollView { 68 public: 69 BoundedScrollView(int min_height, int max_height); 70 71 // Overridden from views::View: 72 virtual gfx::Size GetPreferredSize() OVERRIDE; 73 virtual int GetHeightForWidth(int width) OVERRIDE; 74 virtual void Layout() OVERRIDE; 75 76 private: 77 int min_height_; 78 int max_height_; 79 80 DISALLOW_COPY_AND_ASSIGN(BoundedScrollView); 81 }; 82 83 BoundedScrollView::BoundedScrollView(int min_height, int max_height) 84 : min_height_(min_height), 85 max_height_(max_height) { 86 set_notify_enter_exit_on_child(true); 87 set_background( 88 views::Background::CreateSolidBackground(kMessageCenterBackgroundColor)); 89 SetVerticalScrollBar(new views::OverlayScrollBar(false)); 90 } 91 92 gfx::Size BoundedScrollView::GetPreferredSize() { 93 gfx::Size size = contents()->GetPreferredSize(); 94 size.SetToMax(gfx::Size(size.width(), min_height_)); 95 size.SetToMin(gfx::Size(size.width(), max_height_)); 96 gfx::Insets insets = GetInsets(); 97 size.Enlarge(insets.width(), insets.height()); 98 return size; 99 } 100 101 int BoundedScrollView::GetHeightForWidth(int width) { 102 gfx::Insets insets = GetInsets(); 103 width = std::max(0, width - insets.width()); 104 int height = contents()->GetHeightForWidth(width) + insets.height(); 105 return std::min(std::max(height, min_height_), max_height_); 106 } 107 108 void BoundedScrollView::Layout() { 109 int content_width = width(); 110 int content_height = contents()->GetHeightForWidth(content_width); 111 if (content_height > height()) { 112 content_width = std::max(content_width - GetScrollBarWidth(), 0); 113 content_height = contents()->GetHeightForWidth(content_width); 114 } 115 if (contents()->bounds().size() != gfx::Size(content_width, content_height)) 116 contents()->SetBounds(0, 0, content_width, content_height); 117 views::ScrollView::Layout(); 118 } 119 120 class NoNotificationMessageView : public views::View { 121 public: 122 NoNotificationMessageView(); 123 virtual ~NoNotificationMessageView(); 124 125 // Overridden from views::View. 126 virtual gfx::Size GetPreferredSize() OVERRIDE; 127 virtual int GetHeightForWidth(int width) OVERRIDE; 128 virtual void Layout() OVERRIDE; 129 130 private: 131 views::Label* label_; 132 133 DISALLOW_COPY_AND_ASSIGN(NoNotificationMessageView); 134 }; 135 136 NoNotificationMessageView::NoNotificationMessageView() { 137 label_ = new views::Label(l10n_util::GetStringUTF16( 138 IDS_MESSAGE_CENTER_NO_MESSAGES)); 139 label_->SetAutoColorReadabilityEnabled(false); 140 label_->SetEnabledColor(kNoNotificationsTextColor); 141 // Set transparent background to ensure that subpixel rendering 142 // is disabled. See crbug.com/169056 143 #if defined(OS_LINUX) && defined(OS_CHROMEOS) 144 label_->SetBackgroundColor(kTransparentColor); 145 #endif 146 AddChildView(label_); 147 } 148 149 NoNotificationMessageView::~NoNotificationMessageView() { 150 } 151 152 gfx::Size NoNotificationMessageView::GetPreferredSize() { 153 return gfx::Size(kMinScrollViewHeight, label_->GetPreferredSize().width()); 154 } 155 156 int NoNotificationMessageView::GetHeightForWidth(int width) { 157 return kMinScrollViewHeight; 158 } 159 160 void NoNotificationMessageView::Layout() { 161 int text_height = label_->GetHeightForWidth(width()); 162 int margin = (height() - text_height) / 2; 163 label_->SetBounds(0, margin, width(), text_height); 164 } 165 166 // Displays a list of messages for rich notifications. Functions as an array of 167 // MessageViews and animates them on transitions. It also supports 168 // repositioning. 169 class MessageListView : public views::View, 170 public views::BoundsAnimatorObserver { 171 public: 172 explicit MessageListView(MessageCenterView* message_center_view, 173 bool top_down); 174 virtual ~MessageListView(); 175 176 void AddNotificationAt(MessageView* view, int i); 177 void RemoveNotificationAt(int i); 178 void UpdateNotificationAt(MessageView* view, int i); 179 void SetRepositionTarget(const gfx::Rect& target_rect); 180 void ResetRepositionSession(); 181 void ClearAllNotifications(const gfx::Rect& visible_scroll_rect); 182 183 protected: 184 // Overridden from views::View. 185 virtual void Layout() OVERRIDE; 186 virtual gfx::Size GetPreferredSize() OVERRIDE; 187 virtual int GetHeightForWidth(int width) OVERRIDE; 188 virtual void PaintChildren(gfx::Canvas* canvas) OVERRIDE; 189 virtual void ReorderChildLayers(ui::Layer* parent_layer) OVERRIDE; 190 191 // Overridden from views::BoundsAnimatorObserver. 192 virtual void OnBoundsAnimatorProgressed( 193 views::BoundsAnimator* animator) OVERRIDE; 194 virtual void OnBoundsAnimatorDone(views::BoundsAnimator* animator) OVERRIDE; 195 196 private: 197 // Returns the actual index for child of |index|. 198 // MessageListView allows to slide down upper notifications, which means 199 // that the upper ones should come above the lower ones if top_down is not 200 // enabled. To achieve this, inversed order is adopted. The top most 201 // notification is the last child, and the bottom most notification is the 202 // first child. 203 int GetActualIndex(int index); 204 bool IsValidChild(views::View* child); 205 void DoUpdateIfPossible(); 206 207 // Animates all notifications below target upwards to align with the top of 208 // the last closed notification. 209 void AnimateNotificationsBelowTarget(); 210 // Animates all notifications above target downwards to align with the top of 211 // the last closed notification. 212 void AnimateNotificationsAboveTarget(); 213 214 // Schedules animation for a child to the specified position. Returns false 215 // if |child| will disappear after the animation. 216 bool AnimateChild(views::View* child, int top, int height); 217 218 // Animate clearing one notification. 219 void AnimateClearingOneNotification(); 220 MessageCenterView* message_center_view() const { 221 return message_center_view_; 222 } 223 224 MessageCenterView* message_center_view_; // Weak reference. 225 // The top position of the reposition target rectangle. 226 int reposition_top_; 227 int fixed_height_; 228 bool has_deferred_task_; 229 bool clear_all_started_; 230 bool top_down_; 231 std::set<views::View*> adding_views_; 232 std::set<views::View*> deleting_views_; 233 std::set<views::View*> deleted_when_done_; 234 std::list<views::View*> clearing_all_views_; 235 scoped_ptr<views::BoundsAnimator> animator_; 236 base::WeakPtrFactory<MessageListView> weak_ptr_factory_; 237 238 DISALLOW_COPY_AND_ASSIGN(MessageListView); 239 }; 240 241 MessageListView::MessageListView(MessageCenterView* message_center_view, 242 bool top_down) 243 : message_center_view_(message_center_view), 244 reposition_top_(-1), 245 fixed_height_(0), 246 has_deferred_task_(false), 247 clear_all_started_(false), 248 top_down_(top_down), 249 weak_ptr_factory_(this) { 250 views::BoxLayout* layout = 251 new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 1); 252 layout->set_spread_blank_space(true); 253 SetLayoutManager(layout); 254 255 // Set the margin to 0 for the layout. BoxLayout assumes the same margin 256 // for top and bottom, but the bottom margin here should be smaller 257 // because of the shadow of message view. Use an empty border instead 258 // to provide this margin. 259 gfx::Insets shadow_insets = MessageView::GetShadowInsets(); 260 set_background(views::Background::CreateSolidBackground( 261 kMessageCenterBackgroundColor)); 262 set_border(views::Border::CreateEmptyBorder( 263 top_down ? 0 : kMarginBetweenItems - shadow_insets.top(), /* top */ 264 kMarginBetweenItems - shadow_insets.left(), /* left */ 265 top_down ? kMarginBetweenItems - shadow_insets.bottom() : 0, /* bottom */ 266 kMarginBetweenItems - shadow_insets.right() /* right */)); 267 } 268 269 MessageListView::~MessageListView() { 270 if (animator_.get()) 271 animator_->RemoveObserver(this); 272 } 273 274 void MessageListView::Layout() { 275 if (animator_.get()) 276 return; 277 278 gfx::Rect child_area = GetContentsBounds(); 279 int top = child_area.y(); 280 int between_items = 281 kMarginBetweenItems - MessageView::GetShadowInsets().bottom(); 282 283 for (int i = 0; i < child_count(); ++i) { 284 views::View* child = child_at(i); 285 if (!child->visible()) 286 continue; 287 int height = child->GetHeightForWidth(child_area.width()); 288 child->SetBounds(child_area.x(), top, child_area.width(), height); 289 top += height + between_items; 290 } 291 } 292 293 void MessageListView::AddNotificationAt(MessageView* view, int i) { 294 AddChildViewAt(view, GetActualIndex(i)); 295 if (GetContentsBounds().IsEmpty()) 296 return; 297 298 adding_views_.insert(view); 299 DoUpdateIfPossible(); 300 } 301 302 void MessageListView::RemoveNotificationAt(int i) { 303 views::View* child = child_at(GetActualIndex(i)); 304 if (GetContentsBounds().IsEmpty()) { 305 delete child; 306 } else { 307 if (child->layer()) { 308 deleting_views_.insert(child); 309 } else { 310 if (animator_.get()) 311 animator_->StopAnimatingView(child); 312 delete child; 313 } 314 DoUpdateIfPossible(); 315 } 316 } 317 318 void MessageListView::UpdateNotificationAt(MessageView* view, int i) { 319 int actual_index = GetActualIndex(i); 320 views::View* child = child_at(actual_index); 321 if (animator_.get()) 322 animator_->StopAnimatingView(child); 323 gfx::Rect old_bounds = child->bounds(); 324 if (deleting_views_.find(child) != deleting_views_.end()) 325 deleting_views_.erase(child); 326 if (deleted_when_done_.find(child) != deleted_when_done_.end()) 327 deleted_when_done_.erase(child); 328 delete child; 329 AddChildViewAt(view, actual_index); 330 view->SetBounds(old_bounds.x(), old_bounds.y(), old_bounds.width(), 331 view->GetHeightForWidth(old_bounds.width())); 332 DoUpdateIfPossible(); 333 } 334 335 gfx::Size MessageListView::GetPreferredSize() { 336 int width = 0; 337 for (int i = 0; i < child_count(); i++) { 338 views::View* child = child_at(i); 339 if (IsValidChild(child)) 340 width = std::max(width, child->GetPreferredSize().width()); 341 } 342 343 return gfx::Size(width + GetInsets().width(), 344 GetHeightForWidth(width + GetInsets().width())); 345 } 346 347 int MessageListView::GetHeightForWidth(int width) { 348 if (fixed_height_ > 0) 349 return fixed_height_; 350 351 width -= GetInsets().width(); 352 int height = 0; 353 int padding = 0; 354 for (int i = 0; i < child_count(); ++i) { 355 views::View* child = child_at(i); 356 if (!IsValidChild(child)) 357 continue; 358 height += child->GetHeightForWidth(width) + padding; 359 padding = kMarginBetweenItems - MessageView::GetShadowInsets().bottom(); 360 } 361 362 return height + GetInsets().height(); 363 } 364 365 void MessageListView::PaintChildren(gfx::Canvas* canvas) { 366 // Paint in the inversed order. Otherwise upper notification may be 367 // hidden by the lower one. 368 for (int i = child_count() - 1; i >= 0; --i) { 369 if (!child_at(i)->layer()) 370 child_at(i)->Paint(canvas); 371 } 372 } 373 374 void MessageListView::ReorderChildLayers(ui::Layer* parent_layer) { 375 // Reorder children to stack the last child layer at the top. Otherwise 376 // upper notification may be hidden by the lower one. 377 for (int i = 0; i < child_count(); ++i) { 378 if (child_at(i)->layer()) 379 parent_layer->StackAtBottom(child_at(i)->layer()); 380 } 381 } 382 383 void MessageListView::SetRepositionTarget(const gfx::Rect& target) { 384 reposition_top_ = target.y(); 385 fixed_height_ = GetHeightForWidth(width()); 386 } 387 388 void MessageListView::ResetRepositionSession() { 389 // Don't call DoUpdateIfPossible(), but let Layout() do the task without 390 // animation. Reset will cause the change of the bubble size itself, and 391 // animation from the old location will look weird. 392 if (reposition_top_ >= 0 && animator_.get()) { 393 has_deferred_task_ = false; 394 // cancel cause OnBoundsAnimatorDone which deletes |deleted_when_done_|. 395 animator_->Cancel(); 396 STLDeleteContainerPointers(deleting_views_.begin(), deleting_views_.end()); 397 deleting_views_.clear(); 398 adding_views_.clear(); 399 animator_.reset(); 400 } 401 402 reposition_top_ = -1; 403 fixed_height_ = 0; 404 } 405 406 void MessageListView::ClearAllNotifications( 407 const gfx::Rect& visible_scroll_rect) { 408 for (int i = 0; i < child_count(); ++i) { 409 views::View* child = child_at(i); 410 if (!child->visible()) 411 continue; 412 if (gfx::IntersectRects(child->bounds(), visible_scroll_rect).IsEmpty()) 413 continue; 414 clearing_all_views_.push_back(child); 415 } 416 DoUpdateIfPossible(); 417 } 418 419 void MessageListView::OnBoundsAnimatorProgressed( 420 views::BoundsAnimator* animator) { 421 DCHECK_EQ(animator_.get(), animator); 422 for (std::set<views::View*>::iterator iter = deleted_when_done_.begin(); 423 iter != deleted_when_done_.end(); ++iter) { 424 const gfx::SlideAnimation* animation = animator->GetAnimationForView(*iter); 425 if (animation) 426 (*iter)->layer()->SetOpacity(animation->CurrentValueBetween(1.0, 0.0)); 427 } 428 } 429 430 void MessageListView::OnBoundsAnimatorDone(views::BoundsAnimator* animator) { 431 STLDeleteContainerPointers( 432 deleted_when_done_.begin(), deleted_when_done_.end()); 433 deleted_when_done_.clear(); 434 435 if (clear_all_started_) { 436 clear_all_started_ = false; 437 message_center_view()->OnAllNotificationsCleared(); 438 } 439 440 if (has_deferred_task_) { 441 has_deferred_task_ = false; 442 DoUpdateIfPossible(); 443 } 444 445 if (GetWidget()) 446 GetWidget()->SynthesizeMouseMoveEvent(); 447 } 448 449 int MessageListView::GetActualIndex(int index) { 450 for (int i = 0; i < child_count() && i <= index; ++i) 451 index += IsValidChild(child_at(i)) ? 0 : 1; 452 return std::min(index, child_count()); 453 } 454 455 bool MessageListView::IsValidChild(views::View* child) { 456 return child->visible() && 457 deleting_views_.find(child) == deleting_views_.end() && 458 deleted_when_done_.find(child) == deleted_when_done_.end(); 459 } 460 461 void MessageListView::DoUpdateIfPossible() { 462 gfx::Rect child_area = GetContentsBounds(); 463 if (child_area.IsEmpty()) 464 return; 465 466 if (animator_.get() && animator_->IsAnimating()) { 467 has_deferred_task_ = true; 468 return; 469 } 470 471 if (!animator_.get()) { 472 animator_.reset(new views::BoundsAnimator(this)); 473 animator_->AddObserver(this); 474 } 475 476 if (!clearing_all_views_.empty()) { 477 AnimateClearingOneNotification(); 478 return; 479 } 480 481 if (top_down_) 482 AnimateNotificationsBelowTarget(); 483 else 484 AnimateNotificationsAboveTarget(); 485 486 adding_views_.clear(); 487 deleting_views_.clear(); 488 } 489 490 void MessageListView::AnimateNotificationsBelowTarget() { 491 int last_index = -1; 492 for (int i = 0; i < child_count(); ++i) { 493 views::View* child = child_at(i); 494 if (!IsValidChild(child)) { 495 AnimateChild(child, child->y(), child->height()); 496 } else if (reposition_top_ < 0 || child->y() > reposition_top_) { 497 // Find first notification below target (or all notifications if no 498 // target). 499 last_index = i; 500 break; 501 } 502 } 503 if (last_index > 0) { 504 int between_items = 505 kMarginBetweenItems - MessageView::GetShadowInsets().bottom(); 506 int top = (reposition_top_ > 0) ? reposition_top_ : GetInsets().top(); 507 508 for (int i = last_index; i < child_count(); ++i) { 509 // Animate notifications below target upwards. 510 views::View* child = child_at(i); 511 if (AnimateChild(child, top, child->height())) 512 top += child->height() + between_items; 513 } 514 } 515 } 516 517 void MessageListView::AnimateNotificationsAboveTarget() { 518 int last_index = -1; 519 for (int i = child_count() - 1; i >= 0; --i) { 520 views::View* child = child_at(i); 521 if (!IsValidChild(child)) { 522 AnimateChild(child, child->y(), child->height()); 523 } else if (reposition_top_ < 0 || child->y() < reposition_top_) { 524 // Find first notification above target (or all notifications if no 525 // target). 526 last_index = i; 527 break; 528 } 529 } 530 if (last_index >= 0) { 531 int between_items = 532 kMarginBetweenItems - MessageView::GetShadowInsets().bottom(); 533 int bottom = (reposition_top_ > 0) 534 ? reposition_top_ + child_at(last_index)->height() 535 : GetHeightForWidth(width()) - GetInsets().bottom(); 536 for (int i = last_index; i >= 0; --i) { 537 // Animate notifications above target downwards. 538 views::View* child = child_at(i); 539 if (AnimateChild(child, bottom - child->height(), child->height())) 540 bottom -= child->height() + between_items; 541 } 542 } 543 } 544 545 bool MessageListView::AnimateChild(views::View* child, int top, int height) { 546 gfx::Rect child_area = GetContentsBounds(); 547 if (adding_views_.find(child) != adding_views_.end()) { 548 child->SetBounds(child_area.right(), top, child_area.width(), height); 549 animator_->AnimateViewTo( 550 child, gfx::Rect(child_area.x(), top, child_area.width(), height)); 551 } else if (deleting_views_.find(child) != deleting_views_.end()) { 552 DCHECK(child->layer()); 553 // No moves, but animate to fade-out. 554 animator_->AnimateViewTo(child, child->bounds()); 555 deleted_when_done_.insert(child); 556 return false; 557 } else { 558 gfx::Rect target(child_area.x(), top, child_area.width(), height); 559 if (child->bounds().origin() != target.origin()) 560 animator_->AnimateViewTo(child, target); 561 else 562 child->SetBoundsRect(target); 563 } 564 return true; 565 } 566 567 void MessageListView::AnimateClearingOneNotification() { 568 DCHECK(!clearing_all_views_.empty()); 569 570 clear_all_started_ = true; 571 572 views::View* child = clearing_all_views_.front(); 573 clearing_all_views_.pop_front(); 574 575 // Slide from left to right. 576 gfx::Rect new_bounds = child->bounds(); 577 new_bounds.set_x(new_bounds.right() + kMarginBetweenItems); 578 animator_->AnimateViewTo(child, new_bounds); 579 580 // Schedule to start sliding out next notification after a short delay. 581 if (!clearing_all_views_.empty()) { 582 base::MessageLoop::current()->PostDelayedTask( 583 FROM_HERE, 584 base::Bind(&MessageListView::AnimateClearingOneNotification, 585 weak_ptr_factory_.GetWeakPtr()), 586 base::TimeDelta::FromMilliseconds( 587 kAnimateClearingNextNotificationDelayMS)); 588 } 589 } 590 591 // MessageCenterView /////////////////////////////////////////////////////////// 592 593 MessageCenterView::MessageCenterView(MessageCenter* message_center, 594 MessageCenterTray* tray, 595 int max_height, 596 bool initially_settings_visible, 597 bool top_down) 598 : message_center_(message_center), 599 tray_(tray), 600 scroller_(NULL), 601 settings_view_(NULL), 602 button_bar_(NULL), 603 top_down_(top_down), 604 settings_visible_(initially_settings_visible), 605 source_view_(NULL), 606 source_height_(0), 607 target_view_(NULL), 608 target_height_(0), 609 is_closing_(false) { 610 message_center_->AddObserver(this); 611 set_notify_enter_exit_on_child(true); 612 set_background(views::Background::CreateSolidBackground( 613 kMessageCenterBackgroundColor)); 614 615 NotifierSettingsProvider* notifier_settings_provider = 616 message_center_->GetNotifierSettingsProvider(); 617 button_bar_ = new MessageCenterButtonBar(this, 618 message_center, 619 notifier_settings_provider, 620 initially_settings_visible); 621 622 const int button_height = button_bar_->GetPreferredSize().height(); 623 624 scroller_ = 625 new BoundedScrollView(kMinScrollViewHeight, max_height - button_height); 626 627 if (get_use_acceleration_when_possible()) { 628 scroller_->SetPaintToLayer(true); 629 scroller_->SetFillsBoundsOpaquely(false); 630 scroller_->layer()->SetMasksToBounds(true); 631 } 632 633 empty_list_view_.reset(new NoNotificationMessageView); 634 empty_list_view_->set_owned_by_client(); 635 message_list_view_.reset(new MessageListView(this, top_down)); 636 message_list_view_->set_owned_by_client(); 637 638 // We want to swap the contents of the scroll view between the empty list 639 // view and the message list view, without constructing them afresh each 640 // time. So, since the scroll view deletes old contents each time you 641 // set the contents (regardless of the |owned_by_client_| setting) we need 642 // an intermediate view for the contents whose children we can swap in and 643 // out. 644 views::View* scroller_contents = new views::View(); 645 scroller_contents->SetLayoutManager(new views::FillLayout()); 646 scroller_contents->AddChildView(empty_list_view_.get()); 647 scroller_->SetContents(scroller_contents); 648 649 settings_view_ = new NotifierSettingsView(notifier_settings_provider); 650 651 if (initially_settings_visible) 652 scroller_->SetVisible(false); 653 else 654 settings_view_->SetVisible(false); 655 656 AddChildView(scroller_); 657 AddChildView(settings_view_); 658 AddChildView(button_bar_); 659 } 660 661 MessageCenterView::~MessageCenterView() { 662 if (!is_closing_) 663 message_center_->RemoveObserver(this); 664 } 665 666 void MessageCenterView::SetNotifications( 667 const NotificationList::Notifications& notifications) { 668 if (is_closing_) 669 return; 670 671 notification_views_.clear(); 672 673 // Count how many times each Notifier is encountered. We group Notifications 674 // by NotifierId. 675 std::map<NotifierId, int> groups; 676 int index = 0; 677 678 if (IsExperimentalNotificationUIEnabled()) { 679 for (NotificationList::Notifications::const_iterator iter = 680 notifications.begin(); iter != notifications.end(); ++iter) { 681 NotifierId group_id = (*iter)->notifier_id(); 682 std::map<NotifierId, int>::iterator group_iter = groups.find(group_id); 683 if (group_iter != groups.end()) 684 group_iter->second++; 685 else 686 groups[group_id] = 1; 687 } 688 689 // TODO(dimich): Find a better group icon. Preferably associated with 690 // the group (notifier icon?). 691 gfx::ImageSkia* group_icon = ui::ResourceBundle::GetSharedInstance(). 692 GetImageSkiaNamed(IDR_FOLDER_CLOSED); 693 694 for (NotificationList::Notifications::const_iterator iter = 695 notifications.begin(); iter != notifications.end(); ++iter) { 696 // See if the notification's NotifierId is encountered too many 697 // times - in this case replace all notifications from this source with 698 // a synthetic placeholder that says "N more". Mark the NotifierId 699 // as "seen" by setting count to 0 so the subsequent notificaitons from 700 // the same source are ignored. 701 std::map<NotifierId, int>::iterator group_iter = 702 groups.find((*iter)->notifier_id()); 703 // We should have collected all groups in the loop above. 704 DCHECK(group_iter != groups.end()); 705 706 if (group_iter->second > kMaxNotificationCountFromSingleDisplaySource) { 707 AddGroupPlaceholder(group_iter->first, 708 *(*iter), 709 group_icon ? *group_icon : gfx::ImageSkia(), 710 group_iter->second, 711 index++); 712 group_iter->second = 0; // Mark. 713 } else if (group_iter->second == 0) { // Marked, skip. 714 continue; 715 } else { // Ungrouped notifications 716 AddNotificationAt(*(*iter), index++); 717 } 718 719 message_center_->DisplayedNotification((*iter)->id()); 720 if (notification_views_.size() >= kMaxVisibleMessageCenterNotifications) 721 break; 722 } 723 } else { 724 for (NotificationList::Notifications::const_iterator iter = 725 notifications.begin(); iter != notifications.end(); ++iter) { 726 AddNotificationAt(*(*iter), index++); 727 728 message_center_->DisplayedNotification((*iter)->id()); 729 if (notification_views_.size() >= kMaxVisibleMessageCenterNotifications) 730 break; 731 } 732 } 733 734 NotificationsChanged(); 735 scroller_->RequestFocus(); 736 } 737 738 void MessageCenterView::SetSettingsVisible(bool visible) { 739 if (is_closing_) 740 return; 741 742 if (visible == settings_visible_) 743 return; 744 745 settings_visible_ = visible; 746 747 if (visible) { 748 source_view_ = scroller_; 749 target_view_ = settings_view_; 750 } else { 751 source_view_ = settings_view_; 752 target_view_ = scroller_; 753 } 754 source_height_ = source_view_->GetHeightForWidth(width()); 755 target_height_ = target_view_->GetHeightForWidth(width()); 756 757 gfx::MultiAnimation::Parts parts; 758 // First part: slide resize animation. 759 parts.push_back(gfx::MultiAnimation::Part( 760 (source_height_ == target_height_) ? 0 : kDefaultAnimationDurationMs, 761 gfx::Tween::EASE_OUT)); 762 // Second part: fade-out the source_view. 763 if (source_view_->layer()) { 764 parts.push_back(gfx::MultiAnimation::Part( 765 kDefaultAnimationDurationMs, gfx::Tween::LINEAR)); 766 } else { 767 parts.push_back(gfx::MultiAnimation::Part()); 768 } 769 // Third part: fade-in the target_view. 770 if (target_view_->layer()) { 771 parts.push_back(gfx::MultiAnimation::Part( 772 kDefaultAnimationDurationMs, gfx::Tween::LINEAR)); 773 target_view_->layer()->SetOpacity(0); 774 target_view_->SetVisible(true); 775 } else { 776 parts.push_back(gfx::MultiAnimation::Part()); 777 } 778 settings_transition_animation_.reset(new gfx::MultiAnimation( 779 parts, base::TimeDelta::FromMicroseconds(1000000 / kDefaultFrameRateHz))); 780 settings_transition_animation_->set_delegate(this); 781 settings_transition_animation_->set_continuous(false); 782 settings_transition_animation_->Start(); 783 784 button_bar_->SetBackArrowVisible(visible); 785 } 786 787 void MessageCenterView::ClearAllNotifications() { 788 if (is_closing_) 789 return; 790 791 scroller_->SetEnabled(false); 792 button_bar_->SetAllButtonsEnabled(false); 793 message_list_view_->ClearAllNotifications(scroller_->GetVisibleRect()); 794 } 795 796 void MessageCenterView::OnAllNotificationsCleared() { 797 scroller_->SetEnabled(true); 798 button_bar_->SetAllButtonsEnabled(true); 799 button_bar_->SetCloseAllButtonEnabled(false); 800 message_center_->RemoveAllVisibleNotifications(true); // Action by user. 801 } 802 803 size_t MessageCenterView::NumMessageViewsForTest() const { 804 return message_list_view_->child_count(); 805 } 806 807 void MessageCenterView::OnSettingsChanged() { 808 scroller_->InvalidateLayout(); 809 PreferredSizeChanged(); 810 Layout(); 811 } 812 813 void MessageCenterView::SetIsClosing(bool is_closing) { 814 is_closing_ = is_closing; 815 if (is_closing) 816 message_center_->RemoveObserver(this); 817 else 818 message_center_->AddObserver(this); 819 } 820 821 void MessageCenterView::Layout() { 822 if (is_closing_) 823 return; 824 825 int button_height = button_bar_->GetHeightForWidth(width()) + 826 button_bar_->GetInsets().height(); 827 // Skip unnecessary re-layout of contents during the resize animation. 828 bool animating = settings_transition_animation_ && 829 settings_transition_animation_->is_animating(); 830 if (animating && settings_transition_animation_->current_part_index() == 0) { 831 if (!top_down_) { 832 button_bar_->SetBounds( 833 0, height() - button_height, width(), button_height); 834 } 835 return; 836 } 837 838 scroller_->SetBounds(0, 839 top_down_ ? button_height : 0, 840 width(), 841 height() - button_height); 842 settings_view_->SetBounds(0, 843 top_down_ ? button_height : 0, 844 width(), 845 height() - button_height); 846 847 bool is_scrollable = false; 848 if (scroller_->visible()) 849 is_scrollable = scroller_->height() < message_list_view_->height(); 850 else 851 is_scrollable = settings_view_->IsScrollable(); 852 853 if (!animating) { 854 if (is_scrollable) { 855 // Draw separator line on the top of the button bar if it is on the bottom 856 // or draw it at the bottom if the bar is on the top. 857 button_bar_->set_border(views::Border::CreateSolidSidedBorder( 858 top_down_ ? 0 : 1, 0, top_down_ ? 1 : 0, 0, kFooterDelimiterColor)); 859 } else { 860 button_bar_->set_border(views::Border::CreateEmptyBorder( 861 top_down_ ? 0 : 1, 0, top_down_ ? 1 : 0, 0)); 862 } 863 button_bar_->SchedulePaint(); 864 } 865 button_bar_->SetBounds(0, 866 top_down_ ? 0 : height() - button_height, 867 width(), 868 button_height); 869 if (GetWidget()) 870 GetWidget()->GetRootView()->SchedulePaint(); 871 } 872 873 gfx::Size MessageCenterView::GetPreferredSize() { 874 if (settings_transition_animation_ && 875 settings_transition_animation_->is_animating()) { 876 int content_width = std::max(source_view_->GetPreferredSize().width(), 877 target_view_->GetPreferredSize().width()); 878 int width = std::max(content_width, 879 button_bar_->GetPreferredSize().width()); 880 return gfx::Size(width, GetHeightForWidth(width)); 881 } 882 883 int width = 0; 884 for (int i = 0; i < child_count(); ++i) { 885 views::View* child = child_at(0); 886 if (child->visible()) 887 width = std::max(width, child->GetPreferredSize().width()); 888 } 889 return gfx::Size(width, GetHeightForWidth(width)); 890 } 891 892 int MessageCenterView::GetHeightForWidth(int width) { 893 if (settings_transition_animation_ && 894 settings_transition_animation_->is_animating()) { 895 int content_height = target_height_; 896 if (settings_transition_animation_->current_part_index() == 0) { 897 content_height = settings_transition_animation_->CurrentValueBetween( 898 source_height_, target_height_); 899 } 900 return button_bar_->GetHeightForWidth(width) + content_height; 901 } 902 903 int content_height = 0; 904 if (scroller_->visible()) 905 content_height += scroller_->GetHeightForWidth(width); 906 else 907 content_height += settings_view_->GetHeightForWidth(width); 908 return button_bar_->GetHeightForWidth(width) + 909 button_bar_->GetInsets().height() + content_height; 910 } 911 912 bool MessageCenterView::OnMouseWheel(const ui::MouseWheelEvent& event) { 913 // Do not rely on the default scroll event handler of ScrollView because 914 // the scroll happens only when the focus is on the ScrollView. The 915 // notification center will allow the scrolling even when the focus is on 916 // the buttons. 917 if (scroller_->bounds().Contains(event.location())) 918 return scroller_->OnMouseWheel(event); 919 return views::View::OnMouseWheel(event); 920 } 921 922 void MessageCenterView::OnMouseExited(const ui::MouseEvent& event) { 923 if (is_closing_) 924 return; 925 926 message_list_view_->ResetRepositionSession(); 927 NotificationsChanged(); 928 } 929 930 // TODO(dimich): update for GROUP_VIEW 931 void MessageCenterView::OnNotificationAdded(const std::string& id) { 932 int index = 0; 933 const NotificationList::Notifications& notifications = 934 message_center_->GetVisibleNotifications(); 935 for (NotificationList::Notifications::const_iterator iter = 936 notifications.begin(); iter != notifications.end(); 937 ++iter, ++index) { 938 if ((*iter)->id() == id) { 939 AddNotificationAt(*(*iter), index); 940 break; 941 } 942 if (notification_views_.size() >= kMaxVisibleMessageCenterNotifications) 943 break; 944 } 945 NotificationsChanged(); 946 } 947 948 // TODO(dimich): update for GROUP_VIEW 949 void MessageCenterView::OnNotificationRemoved(const std::string& id, 950 bool by_user) { 951 NotificationViewsMap::iterator view_iter = notification_views_.find(id); 952 if (view_iter == notification_views_.end()) 953 return; 954 NotificationView* view = view_iter->second; 955 int index = message_list_view_->GetIndexOf(view); 956 if (by_user) { 957 message_list_view_->SetRepositionTarget(view->bounds()); 958 // Moves the keyboard focus to the next notification if the removed 959 // notification is focused so that the user can dismiss notifications 960 // without re-focusing by tab key. 961 if (view->IsCloseButtonFocused() || 962 view == GetFocusManager()->GetFocusedView()) { 963 views::View* next_focused_view = NULL; 964 if (message_list_view_->child_count() > index + 1) 965 next_focused_view = message_list_view_->child_at(index + 1); 966 else if (index > 0) 967 next_focused_view = message_list_view_->child_at(index - 1); 968 969 if (next_focused_view) { 970 if (view->IsCloseButtonFocused()) 971 // Safe cast since all views in MessageListView are MessageViews. 972 static_cast<MessageView*>( 973 next_focused_view)->RequestFocusOnCloseButton(); 974 else 975 next_focused_view->RequestFocus(); 976 } 977 } 978 } 979 message_list_view_->RemoveNotificationAt(index); 980 notification_views_.erase(view_iter); 981 NotificationsChanged(); 982 } 983 984 // TODO(dimich): update for GROUP_VIEW 985 void MessageCenterView::OnNotificationUpdated(const std::string& id) { 986 NotificationViewsMap::const_iterator view_iter = notification_views_.find(id); 987 if (view_iter == notification_views_.end()) 988 return; 989 NotificationView* view = view_iter->second; 990 size_t index = message_list_view_->GetIndexOf(view); 991 DCHECK(index >= 0); 992 // TODO(dimich): add MessageCenter::GetVisibleNotificationById(id) 993 const NotificationList::Notifications& notifications = 994 message_center_->GetVisibleNotifications(); 995 for (NotificationList::Notifications::const_iterator iter = 996 notifications.begin(); iter != notifications.end(); ++iter) { 997 if ((*iter)->id() == id) { 998 bool expanded = true; 999 if (IsExperimentalNotificationUIEnabled()) 1000 expanded = (*iter)->is_expanded(); 1001 NotificationView* view = 1002 NotificationView::Create(this, 1003 *(*iter), 1004 expanded, 1005 false); // Not creating a top-level 1006 // notification. 1007 view->set_scroller(scroller_); 1008 message_list_view_->UpdateNotificationAt(view, index); 1009 notification_views_[id] = view; 1010 NotificationsChanged(); 1011 break; 1012 } 1013 } 1014 } 1015 1016 void MessageCenterView::ClickOnNotification( 1017 const std::string& notification_id) { 1018 message_center_->ClickOnNotification(notification_id); 1019 } 1020 1021 void MessageCenterView::RemoveNotification(const std::string& notification_id, 1022 bool by_user) { 1023 message_center_->RemoveNotification(notification_id, by_user); 1024 } 1025 1026 void MessageCenterView::DisableNotificationsFromThisSource( 1027 const NotifierId& notifier_id) { 1028 message_center_->DisableNotificationsByNotifier(notifier_id); 1029 } 1030 1031 void MessageCenterView::ShowNotifierSettingsBubble() { 1032 tray_->ShowNotifierSettingsBubble(); 1033 } 1034 1035 bool MessageCenterView::HasClickedListener(const std::string& notification_id) { 1036 return message_center_->HasClickedListener(notification_id); 1037 } 1038 1039 void MessageCenterView::ClickOnNotificationButton( 1040 const std::string& notification_id, 1041 int button_index) { 1042 message_center_->ClickOnNotificationButton(notification_id, button_index); 1043 } 1044 1045 void MessageCenterView::ExpandNotification(const std::string& notification_id) { 1046 message_center_->ExpandNotification(notification_id); 1047 } 1048 1049 void MessageCenterView::GroupBodyClicked( 1050 const std::string& last_notification_id) { 1051 message_center_->ClickOnNotification(last_notification_id); 1052 } 1053 1054 // When clicked on the "N more" button, perform some reasonable action. 1055 // TODO(dimich): find out what the reasonable action could be. 1056 void MessageCenterView::ExpandGroup(const NotifierId& notifier_id) { 1057 NOTIMPLEMENTED(); 1058 } 1059 1060 // Click on Close button on a GroupView should remove all notifications 1061 // represented by this GroupView. 1062 void MessageCenterView::RemoveGroup(const NotifierId& notifier_id) { 1063 std::vector<std::string> notifications_to_remove; 1064 1065 // Can not remove notifications while iterating the list. Collect the ids 1066 // and then run separate loop to remove notifications. 1067 const NotificationList::Notifications& notifications = 1068 message_center_->GetVisibleNotifications(); 1069 for (NotificationList::Notifications::const_iterator iter = 1070 notifications.begin(); iter != notifications.end(); ++iter) { 1071 if ((*iter)->notifier_id() == notifier_id) 1072 notifications_to_remove.push_back((*iter)->id()); 1073 } 1074 1075 for (size_t i = 0; i < notifications_to_remove.size(); ++i) 1076 // "by_user" = true 1077 message_center_->RemoveNotification(notifications_to_remove[i], true); 1078 } 1079 1080 void MessageCenterView::AnimationEnded(const gfx::Animation* animation) { 1081 DCHECK_EQ(animation, settings_transition_animation_.get()); 1082 1083 Visibility visibility = target_view_ == settings_view_ 1084 ? VISIBILITY_SETTINGS 1085 : VISIBILITY_MESSAGE_CENTER; 1086 message_center_->SetVisibility(visibility); 1087 1088 source_view_->SetVisible(false); 1089 target_view_->SetVisible(true); 1090 if (source_view_->layer()) 1091 source_view_->layer()->SetOpacity(1.0); 1092 if (target_view_->layer()) 1093 target_view_->layer()->SetOpacity(1.0); 1094 settings_transition_animation_.reset(); 1095 PreferredSizeChanged(); 1096 Layout(); 1097 } 1098 1099 void MessageCenterView::AnimationProgressed(const gfx::Animation* animation) { 1100 DCHECK_EQ(animation, settings_transition_animation_.get()); 1101 PreferredSizeChanged(); 1102 if (settings_transition_animation_->current_part_index() == 1 && 1103 source_view_->layer()) { 1104 source_view_->layer()->SetOpacity( 1105 1.0 - settings_transition_animation_->GetCurrentValue()); 1106 SchedulePaint(); 1107 } else if (settings_transition_animation_->current_part_index() == 2 && 1108 target_view_->layer()) { 1109 target_view_->layer()->SetOpacity( 1110 settings_transition_animation_->GetCurrentValue()); 1111 SchedulePaint(); 1112 } 1113 } 1114 1115 void MessageCenterView::AnimationCanceled(const gfx::Animation* animation) { 1116 DCHECK_EQ(animation, settings_transition_animation_.get()); 1117 AnimationEnded(animation); 1118 } 1119 1120 1121 void MessageCenterView::AddMessageViewAt(MessageView* view, int index) { 1122 view->set_scroller(scroller_); 1123 message_list_view_->AddNotificationAt(view, index); 1124 } 1125 1126 void MessageCenterView::AddGroupPlaceholder( 1127 const NotifierId& group_id, 1128 const Notification& last_notification, 1129 const gfx::ImageSkia& group_icon, 1130 int group_size, 1131 int index) { 1132 GroupView* view = new GroupView(this, 1133 group_id, 1134 last_notification, 1135 group_icon, 1136 group_size); 1137 group_views_.push_back(view); 1138 AddMessageViewAt(view, index); 1139 } 1140 1141 void MessageCenterView::AddNotificationAt(const Notification& notification, 1142 int index) { 1143 // NotificationViews are expanded by default here until 1144 // http://crbug.com/217902 is fixed. TODO(dharcourt): Fix. 1145 bool expanded = true; 1146 if (IsExperimentalNotificationUIEnabled()) 1147 expanded = notification.is_expanded(); 1148 NotificationView* view = 1149 NotificationView::Create(this, 1150 notification, 1151 expanded, 1152 false); // Not creating a top-level 1153 // notification. 1154 notification_views_[notification.id()] = view; 1155 AddMessageViewAt(view, index); 1156 } 1157 1158 void MessageCenterView::NotificationsChanged() { 1159 bool no_message_views = notification_views_.empty() && group_views_.empty(); 1160 1161 // When the child view is removed from the hierarchy, its focus is cleared. 1162 // In this case we want to save which view has focus so that the user can 1163 // continue to interact with notifications in the order they were expecting. 1164 views::FocusManager* focus_manager = scroller_->GetFocusManager(); 1165 View* focused_view = NULL; 1166 // |focus_manager| can be NULL in tests. 1167 if (focus_manager) 1168 focused_view = focus_manager->GetFocusedView(); 1169 1170 // All the children of this view are owned by |this|. 1171 scroller_->contents()->RemoveAllChildViews(/*delete_children=*/false); 1172 scroller_->contents()->AddChildView( 1173 no_message_views ? empty_list_view_.get() : message_list_view_.get()); 1174 1175 button_bar_->SetCloseAllButtonEnabled(!no_message_views); 1176 scroller_->SetFocusable(!no_message_views); 1177 1178 if (focus_manager && focused_view) 1179 focus_manager->SetFocusedView(focused_view); 1180 1181 scroller_->InvalidateLayout(); 1182 PreferredSizeChanged(); 1183 Layout(); 1184 } 1185 1186 void MessageCenterView::SetNotificationViewForTest(MessageView* view) { 1187 message_list_view_->AddNotificationAt(view, 0); 1188 } 1189 1190 } // namespace message_center 1191