blob: aabf4219ce8614f9dcc5c24c65c15edbd30e0c6b [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/message_center/unified_message_list_view.h"
#include <string>
#include "ash/bubble/bubble_constants.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/system/message_center/ash_notification_view.h"
#include "ash/system/message_center/message_center_constants.h"
#include "ash/system/message_center/message_center_style.h"
#include "ash/system/message_center/message_center_utils.h"
#include "ash/system/message_center/message_view_factory.h"
#include "ash/system/message_center/metrics_utils.h"
#include "ash/system/message_center/notification_swipe_control_view.h"
#include "ash/system/message_center/unified_message_center_view.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/unified/unified_system_tray_model.h"
#include "base/auto_reset.h"
#include "base/callback_forward.h"
#include "base/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/message_center/notification_view_controller.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/views/message_view.h"
#include "ui/views/animation/animation_delegate_views.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"
using message_center::MessageCenter;
using message_center::MessageView;
using message_center::Notification;
namespace ash {
namespace {
constexpr base::TimeDelta kClosingAnimationDuration = base::Milliseconds(320);
constexpr base::TimeDelta kClearAllStackedAnimationDuration =
base::Milliseconds(40);
constexpr base::TimeDelta kClearAllVisibleAnimationDuration =
base::Milliseconds(160);
constexpr char kMessageViewContainerClassName[] = "MessageViewContainer";
constexpr char kMoveDownAnimationSmoothnessHistogramName[] =
"Ash.Notification.MoveDown.AnimationSmoothness";
constexpr char kClearAllStackedAnimationSmoothnessHistogramName[] =
"Ash.Notification.ClearAllStacked.AnimationSmoothness";
constexpr char kClearAllVisibleAnimationSmoothnessHistogramName[] =
"Ash.Notification.ClearAllVisible.AnimationSmoothness";
constexpr char kExpandOrCollapseAnimationSmoothnessHistogramName[] =
"Ash.Notification.ExpandOrCollapse.AnimationSmoothness";
void RecordAnimationSmoothness(const std::string& histogram_name,
int smoothness) {
base::UmaHistogramPercentage(histogram_name, smoothness);
}
void SetupThroughputTrackerForAnimationSmoothness(
views::Widget* widget,
absl::optional<ui::ThroughputTracker>& tracker,
const char* histogram_name) {
// `widget` may not exist in tests.
if (!widget)
return;
tracker.emplace(widget->GetCompositor()->RequestNewThroughputTracker());
tracker->Start(ash::metrics_util::ForSmoothness(
base::BindRepeating(&RecordAnimationSmoothness, histogram_name)));
}
} // namespace
// Container view of notification and swipe control.
// All children of UnifiedMessageListView should be MessageViewContainer.
class UnifiedMessageListView::MessageViewContainer
: public MessageView::Observer,
public views::View {
public:
MessageViewContainer(MessageView* message_view,
UnifiedMessageListView* list_view)
: message_view_(message_view),
list_view_(list_view),
control_view_(new NotificationSwipeControlView(message_view)) {
message_view_->AddObserver(this);
if (!features::IsNotificationsRefreshEnabled()) {
message_view_->SetBackground(
views::CreateSolidBackground(SK_ColorTRANSPARENT));
}
SetLayoutManager(std::make_unique<views::FillLayout>());
AddChildView(control_view_);
AddChildView(message_view_);
}
MessageViewContainer(const MessageViewContainer&) = delete;
MessageViewContainer& operator=(const MessageViewContainer&) = delete;
~MessageViewContainer() override { message_view_->RemoveObserver(this); }
base::TimeDelta GetBoundsAnimationDuration() const {
auto* notification = MessageCenter::Get()->FindNotificationById(
message_view()->notification_id());
if (!notification)
return base::Milliseconds(0);
if (message_view()->GetClassName() == AshNotificationView::kViewClassName) {
return static_cast<const AshNotificationView*>(message_view())
->GetBoundsAnimationDuration(*notification);
}
// TODO(crbug/1278483): ARC notifications will require different animation
// durations. Default to kLargeImageExpandAndCollapseAnimationDuration for
// now.
return base::Milliseconds(kLargeImageExpandAndCollapseAnimationDuration);
}
// Update the border and background corners based on if the notification is
// at the top or the bottom. If `force_update` is true, ignore previous states
// and always update the border.
void UpdateBorder(bool is_top, bool is_bottom, bool force_update) {
if (is_top_ == is_top && is_bottom_ == is_bottom && !force_update)
return;
is_top_ = is_top;
is_bottom_ = is_bottom;
message_view_->SetBorder(
is_bottom ? views::NullBorder()
: views::CreateSolidSidedBorder(
0, 0, kUnifiedNotificationSeparatorThickness, 0,
message_center_style::kSeperatorColor));
const int top_radius = is_top ? kBubbleCornerRadius : 0;
const int bottom_radius = is_bottom ? kBubbleCornerRadius : 0;
message_view_->UpdateCornerRadius(top_radius, bottom_radius);
control_view_->UpdateCornerRadius(top_radius, bottom_radius);
}
// Collapses the notification if its state haven't changed manually by a user.
void Collapse() {
if (!message_view_->IsManuallyExpandedOrCollapsed())
message_view_->SetExpanded(false);
}
// Check if the notification is manually expanded / collapsed before and
// restores the state.
void LoadExpandedState(UnifiedSystemTrayModel* model, bool is_latest) {
DCHECK(model);
base::AutoReset<bool> scoped_reset(&loading_expanded_state_, true);
absl::optional<bool> manually_expanded =
model->GetNotificationExpanded(GetNotificationId());
if (manually_expanded.has_value()) {
message_view_->SetExpanded(manually_expanded.value());
message_view_->SetManuallyExpandedOrCollapsed(true);
} else {
// Expand the latest notification, and collapse all other notifications.
message_view_->SetExpanded(is_latest &&
message_view_->IsAutoExpandingAllowed());
}
}
// Stores if the notification is manually expanded or collapsed so that we can
// restore that when UnifiedSystemTray is reopened.
void StoreExpandedState(UnifiedSystemTrayModel* model) {
DCHECK(model);
if (message_view_->IsManuallyExpandedOrCollapsed()) {
model->SetNotificationExpanded(GetNotificationId(),
message_view_->IsExpanded());
}
}
void SlideOutAndClose() {
is_slid_out_programatically = true;
message_view_->SlideOutAndClose(1 /* direction */);
}
std::string GetNotificationId() const {
return message_view_->notification_id();
}
void UpdateWithNotification(const Notification& notification) {
message_view_->UpdateWithNotification(notification);
}
void CloseSwipeControl() { message_view_->CloseSwipeControl(); }
// Returns if the notification is pinned i.e. can be removed manually.
bool IsPinned() const {
return message_view_->GetMode() == MessageView::Mode::PINNED;
}
// Returns the direction that the notification is swiped out. If swiped to the
// left, it returns -1 and if sipwed to the right, it returns 1. By default
// (i.e. the notification is removed but not by touch gesture), it returns 1.
int GetSlideDirection() const {
return message_view_->GetSlideAmount() < 0 ? -1 : 1;
}
// Allows UnifiedMessageListView to force preferred size to change during
// animations.
void TriggerPreferredSizeChangedForAnimation() {
views::View::PreferredSizeChanged();
}
// views::View:
void ChildPreferredSizeChanged(views::View* child) override {
// If we've already been removed, ignore new child size changes.
if (is_removed_)
return;
// PreferredSizeChanged will trigger
// UnifiedMessageListView::ChildPreferredSizeChanged.
base::ScopedClosureRunner defer_preferred_size_changed(base::BindOnce(
&MessageViewContainer::PreferredSizeChanged, base::Unretained(this)));
if (!features::IsNotificationsRefreshEnabled())
return;
// Ignore non user triggered expand/collapses.
if (loading_expanded_state_)
return;
auto* notification = MessageCenter::Get()->FindNotificationById(
message_view()->notification_id());
if (!notification)
return;
needs_bounds_animation_ = true;
}
gfx::Size CalculatePreferredSize() const override {
if (list_view_->IsAnimatingExpandOrCollapseContainer(this)) {
// Width should never change, only height.
return gfx::Size(list_view_->message_view_width_,
gfx::Tween::IntValueBetween(
list_view_->GetCurrentValue(),
start_bounds_.height(), target_bounds_.height()));
}
return gfx::Size(list_view_->message_view_width_, target_bounds_.height());
}
const char* GetClassName() const override {
return kMessageViewContainerClassName;
}
// MessageView::Observer:
void OnSlideChanged(const std::string& notification_id) override {
control_view_->UpdateButtonsVisibility();
}
void OnPreSlideOut(const std::string& notification_id) override {
if (!is_slid_out_programatically) {
metrics_utils::LogClosedByUser(notification_id, /*is_swipe=*/true,
/*is_popup=*/false);
}
}
void OnSlideOut(const std::string& notification_id) override {
is_slid_out_ = true;
set_is_removed();
list_view_->OnNotificationSlidOut();
}
gfx::Rect start_bounds() const { return start_bounds_; }
gfx::Rect target_bounds() const { return target_bounds_; }
bool is_removed() const { return is_removed_; }
void ResetNeedsBoundsAnimation() { needs_bounds_animation_ = false; }
bool needs_bounds_animation() const { return needs_bounds_animation_; }
void set_start_bounds(const gfx::Rect& start_bounds) {
start_bounds_ = start_bounds;
}
void set_target_bounds(const gfx::Rect& ideal_bounds) {
target_bounds_ = ideal_bounds;
}
void set_is_removed() { is_removed_ = true; }
bool is_slid_out() { return is_slid_out_; }
MessageView* message_view() { return message_view_; }
const MessageView* message_view() const { return message_view_; }
private:
// The bounds that the container starts animating from. If not animating, it's
// ignored.
gfx::Rect start_bounds_;
// The final bounds of the container. If not animating, it's same as the
// actual bounds().
gfx::Rect target_bounds_;
// True when the notification is removed and during slide out animation.
bool is_removed_ = false;
// True if the notification is slid out completely.
bool is_slid_out_ = false;
// True if the notification is slid out through SlideOutAndClose()
// programagically. False if slid out manually by the user.
bool is_slid_out_programatically = false;
// Keeps track if this view is at the top or bottom of `list_view_`. Storing
// this to prevent unnecessary update.
bool is_top_ = false;
bool is_bottom_ = false;
// Whether expanded state is being set programmatically. Used to prevent
// animating programmatic expands which occur on open.
bool loading_expanded_state_ = false;
// Set to flag the view as requiring an expand or collapse animation.
bool needs_bounds_animation_ = false;
MessageView* const message_view_;
UnifiedMessageListView* const list_view_;
NotificationSwipeControlView* const control_view_;
};
UnifiedMessageListView::UnifiedMessageListView(
UnifiedMessageCenterView* message_center_view,
scoped_refptr<UnifiedSystemTrayModel> model)
: views::AnimationDelegateViews(this),
message_center_view_(message_center_view),
model_(model),
animation_(std::make_unique<gfx::LinearAnimation>(this)),
is_notifications_refresh_enabled_(
features::IsNotificationsRefreshEnabled()),
message_view_width_(is_notifications_refresh_enabled_
? kTrayMenuWidth - (2 * kMessageCenterSidePadding)
: kTrayMenuWidth) {
message_center_observation_.Observe(MessageCenter::Get());
animation_->SetCurrentValue(1.0);
if (!is_notifications_refresh_enabled_) {
SetBackground(views::CreateSolidBackground(
message_center_style::kSwipeControlBackgroundColor));
}
}
UnifiedMessageListView::~UnifiedMessageListView() {
DCHECK(model_);
model_->ClearNotificationChanges();
for (auto* view : children())
AsMVC(view)->StoreExpandedState(model_.get());
}
void UnifiedMessageListView::Init() {
DCHECK(model_);
bool is_latest = true;
for (auto* notification :
message_center_utils::GetSortedNotificationsWithOwnView()) {
auto* view =
new MessageViewContainer(CreateMessageView(*notification), this);
view->LoadExpandedState(model_.get(), is_latest);
AddChildViewAt(view, 0);
MessageCenter::Get()->DisplayedNotification(
notification->id(), message_center::DISPLAY_SOURCE_MESSAGE_CENTER);
is_latest = false;
}
UpdateBorders(/*force_update=*/true);
UpdateBounds();
}
void UnifiedMessageListView::ClearAllWithAnimation() {
if (state_ == State::CLEAR_ALL_STACKED || state_ == State::CLEAR_ALL_VISIBLE)
return;
ResetBounds();
UMA_HISTOGRAM_COUNTS_100("ChromeOS.SystemTray.NotificationsRemovedByClearAll",
children().size());
// Record a ClosedByClearAll metric for each notification dismissed.
for (auto* child : children()) {
auto* view = AsMVC(child);
metrics_utils::LogClosedByClearAll(view->GetNotificationId());
}
{
base::AutoReset<bool> auto_reset(&ignore_notification_remove_, true);
message_center::MessageCenter::Get()->RemoveAllNotifications(
true /* by_user */,
message_center::MessageCenter::RemoveType::NON_PINNED);
}
state_ = State::CLEAR_ALL_STACKED;
UpdateClearAllAnimation();
if (state_ != State::IDLE)
StartAnimation();
}
std::vector<message_center::Notification*>
UnifiedMessageListView::GetAllNotifications() const {
std::vector<message_center::Notification*> notifications;
for (views::View* view : children()) {
// The view may be present in the view hierarchy, but deleted in the message
// center.
auto* notification = MessageCenter::Get()->FindVisibleNotificationById(
AsMVC(view)->GetNotificationId());
if (notification)
notifications.insert(notifications.begin(), notification);
}
return notifications;
}
std::vector<std::string> UnifiedMessageListView::GetAllNotificationIds() const {
std::vector<std::string> notifications;
for (views::View* view : children()) {
notifications.insert(notifications.begin(),
AsMVC(view)->GetNotificationId());
}
return notifications;
}
std::vector<message_center::Notification*>
UnifiedMessageListView::GetNotificationsAboveY(int y_offset) const {
std::vector<message_center::Notification*> notifications;
for (views::View* view : children()) {
const int bottom_limit =
view->bounds().y() + kNotificationIconStackThreshold;
if (bottom_limit <= y_offset) {
auto* notification = MessageCenter::Get()->FindVisibleNotificationById(
AsMVC(view)->GetNotificationId());
if (notification)
notifications.insert(notifications.begin(), notification);
}
}
return notifications;
}
std::vector<std::string> UnifiedMessageListView::GetNotificationIdsAboveY(
int y_offset) const {
std::vector<std::string> notifications;
for (views::View* view : children()) {
const int bottom_limit =
view->bounds().y() + kNotificationIconStackThreshold;
if (bottom_limit > y_offset)
continue;
notifications.insert(notifications.begin(),
AsMVC(view)->GetNotificationId());
}
return notifications;
}
std::vector<std::string> UnifiedMessageListView::GetNotificationIdsBelowY(
int y_offset) const {
std::vector<std::string> notifications;
for (views::View* view : children()) {
const int top_of_notification = view->bounds().y();
if (top_of_notification < y_offset)
continue;
notifications.insert(notifications.begin(),
AsMVC(view)->GetNotificationId());
}
return notifications;
}
int UnifiedMessageListView::GetTotalNotificationCount() const {
return static_cast<int>(children().size());
}
int UnifiedMessageListView::GetTotalPinnedNotificationCount() const {
int count = 0;
for (auto* child : children()) {
if (AsMVC(child)->IsPinned())
count++;
}
return count;
}
bool UnifiedMessageListView::IsAnimating() const {
return animation_->is_animating();
}
bool UnifiedMessageListView::IsAnimatingExpandOrCollapseContainer(
const views::View* view) const {
if (!view || !expand_or_collapsing_container_)
return false;
DCHECK_EQ(kMessageViewContainerClassName, view->GetClassName())
<< view->GetClassName() << " is not a " << kMessageViewContainerClassName;
const MessageViewContainer* message_view_container = AsMVC(view);
return message_view_container == expand_or_collapsing_container_;
}
void UnifiedMessageListView::ChildPreferredSizeChanged(views::View* child) {
if (ignore_size_change_)
return;
// No State::EXPAND_OR_COLLAPSE animation in the old UI.
if (!features::IsNotificationsRefreshEnabled()) {
ResetBounds();
return;
}
auto* message_view_container = AsMVC(child);
// Immediately complete the old expand/collapse animation. It will be snapped
// to the target bounds when UpdateBounds() is called. If the other animations
// are occurring, prefer them over expand/collapse.
if (message_view_container->needs_bounds_animation() &&
(state_ == State::IDLE || state_ == State::EXPAND_OR_COLLAPSE)) {
if (animation_->is_animating()) {
// Finish the previous expand animation instantly.
animation_->End();
}
expand_or_collapsing_container_ = message_view_container;
expand_or_collapsing_container_->ResetNeedsBoundsAnimation();
UpdateBounds();
state_ = State::EXPAND_OR_COLLAPSE;
StartAnimation();
return;
}
if (state_ == State::EXPAND_OR_COLLAPSE)
return;
ResetBounds();
}
void UnifiedMessageListView::PreferredSizeChanged() {
views::View::PreferredSizeChanged();
if (message_center_view_)
message_center_view_->ListPreferredSizeChanged();
}
void UnifiedMessageListView::Layout() {
for (auto* child : children()) {
auto* view = AsMVC(child);
if (state_ == State::IDLE) {
view->SetBoundsRect(view->target_bounds());
continue;
}
view->SetBoundsRect(gfx::Tween::RectValueBetween(
GetCurrentValue(), view->start_bounds(), view->target_bounds()));
}
}
gfx::Rect UnifiedMessageListView::GetNotificationBounds(
const std::string& notification_id) const {
const MessageViewContainer* child = nullptr;
if (!notification_id.empty())
child = GetNotificationById(notification_id);
return child ? child->bounds() : GetLastNotificationBounds();
}
gfx::Rect UnifiedMessageListView::GetLastNotificationBounds() const {
return children().empty() ? gfx::Rect() : children().back()->bounds();
}
gfx::Rect UnifiedMessageListView::GetNotificationBoundsBelowY(
int y_offset) const {
const auto it = std::find_if(children().cbegin(), children().cend(),
[y_offset](const views::View* v) {
return v->bounds().bottom() >= y_offset;
});
return (it == children().cend()) ? gfx::Rect() : (*it)->bounds();
}
gfx::Size UnifiedMessageListView::CalculatePreferredSize() const {
if (state_ == State::IDLE)
return gfx::Size(message_view_width_, target_height_);
return gfx::Size(message_view_width_,
gfx::Tween::IntValueBetween(GetCurrentValue(), start_height_,
target_height_));
}
const char* UnifiedMessageListView::GetClassName() const {
return "UnifiedMessageListView";
}
message_center::MessageView*
UnifiedMessageListView::GetMessageViewForNotificationId(const std::string& id) {
auto it =
std::find_if(children().begin(), children().end(), [&](auto* child) {
DCHECK(child->GetClassName() == kMessageViewContainerClassName);
return static_cast<MessageViewContainer*>(child)
->message_view()
->notification_id() == id;
});
if (it == children().end())
return nullptr;
return static_cast<MessageViewContainer*>(*it)->message_view();
}
void UnifiedMessageListView::ConvertNotificationViewToGroupedNotificationView(
const std::string& ungrouped_notification_id,
const std::string& new_grouped_notification_id) {
GetMessageViewForNotificationId(ungrouped_notification_id)
->set_notification_id(new_grouped_notification_id);
}
void UnifiedMessageListView::ConvertGroupedNotificationViewToNotificationView(
const std::string& grouped_notification_id,
const std::string& new_single_notification_id) {
GetMessageViewForNotificationId(grouped_notification_id)
->set_notification_id(new_single_notification_id);
}
void UnifiedMessageListView::OnNotificationAdded(const std::string& id) {
auto* notification = MessageCenter::Get()->FindVisibleNotificationById(id);
if (!notification)
return;
InterruptClearAll();
// Collapse all notifications before adding new one.
CollapseAllNotifications();
// Find the correct index to insert the new notification based on the sorted
// order.
auto child_views = children();
size_t index_to_insert = child_views.size();
for (size_t i = 0; i < child_views.size(); ++i) {
MessageViewContainer* message_view =
static_cast<MessageViewContainer*>(child_views[i]);
auto* child_notification =
MessageCenter::Get()->FindVisibleNotificationById(
message_view->GetNotificationId());
if (!child_notification)
break;
if (!message_center_utils::CompareNotifications(notification,
child_notification)) {
index_to_insert = i;
break;
}
}
auto* view = CreateMessageView(*notification);
view->SetExpanded(view->IsAutoExpandingAllowed());
AddChildViewAt(new MessageViewContainer(view, this), index_to_insert);
UpdateBorders(/*force_update=*/false);
ResetBounds();
}
void UnifiedMessageListView::OnNotificationRemoved(const std::string& id,
bool by_user) {
if (ignore_notification_remove_)
return;
// The corresponding MessageView may have already been deleted after being
// manually slid out.
auto* child = GetNotificationById(id);
if (!child)
return;
InterruptClearAll();
ResetBounds();
child->set_is_removed();
// If the MessageView is slid out, then do nothing here. The MOVE_DOWN
// animation will be started in OnNotificationSlidOut().
if (!child->is_slid_out())
child->SlideOutAndClose();
}
void UnifiedMessageListView::OnNotificationSlidOut() {
DeleteRemovedNotifications();
// |message_center_view_| can be null in tests.
if (message_center_view_)
message_center_view_->OnNotificationSlidOut();
state_ = State::MOVE_DOWN;
UpdateBounds();
StartAnimation();
}
void UnifiedMessageListView::OnNotificationUpdated(const std::string& id) {
auto* notification = MessageCenter::Get()->FindVisibleNotificationById(id);
if (!notification)
return;
InterruptClearAll();
// The corresponding MessageView may have been slid out and deleted, so just
// ignore this update as the notification will soon be deleted.
auto* child = GetNotificationById(id);
if (!child)
return;
child->UpdateWithNotification(*notification);
ResetBounds();
}
void UnifiedMessageListView::OnSlideStarted(
const std::string& notification_id) {
// When the swipe control for |notification_id| is shown, hide all other swipe
// controls.
for (auto* child : children()) {
auto* view = AsMVC(child);
if (view->GetNotificationId() != notification_id)
view->CloseSwipeControl();
}
}
void UnifiedMessageListView::OnCloseButtonPressed(
const std::string& notification_id) {
metrics_utils::LogClosedByUser(notification_id, /*is_swipe=*/false,
/*is_popup=*/false);
}
void UnifiedMessageListView::OnSettingsButtonPressed(
const std::string& notification_id) {
metrics_utils::LogSettingsShown(notification_id, /*is_slide_controls=*/false,
/*is_popup=*/false);
}
void UnifiedMessageListView::OnSnoozeButtonPressed(
const std::string& notification_id) {
metrics_utils::LogSnoozed(notification_id, /*is_slide_controls=*/false,
/*is_popup=*/false);
}
void UnifiedMessageListView::AnimationEnded(const gfx::Animation* animation) {
if (throughput_tracker_) {
// Reset `throughput_tracker_` to reset animation metrics recording.
throughput_tracker_->Stop();
throughput_tracker_.reset();
}
// This is also called from AnimationCanceled().
// TODO(crbug/1272104): Can we do better? If we are interrupting an animation,
// this does not look good.
animation_->SetCurrentValue(1.0);
PreferredSizeChanged();
switch (state_) {
case State::IDLE:
case State::EXPAND_OR_COLLAPSE:
expand_or_collapsing_container_ = nullptr;
[[fallthrough]];
case State::MOVE_DOWN:
state_ = State::IDLE;
break;
case State::CLEAR_ALL_STACKED:
case State::CLEAR_ALL_VISIBLE:
DeleteRemovedNotifications();
UpdateClearAllAnimation();
break;
}
UpdateBorders(/*force_update=*/false);
if (state_ != State::IDLE)
StartAnimation();
}
void UnifiedMessageListView::AnimationProgressed(
const gfx::Animation* animation) {
if (state_ == State::EXPAND_OR_COLLAPSE)
expand_or_collapsing_container_->TriggerPreferredSizeChangedForAnimation();
PreferredSizeChanged();
}
void UnifiedMessageListView::AnimationCanceled(
const gfx::Animation* animation) {
AnimationEnded(animation);
}
MessageView* UnifiedMessageListView::CreateMessageView(
const Notification& notification) {
auto* view =
MessageViewFactory::Create(notification, /*shown_in_popup=*/false)
.release();
view->SetIsNested();
view->AddObserver(this);
message_center_view_->ConfigureMessageView(view);
return view;
}
std::vector<message_center::Notification*>
UnifiedMessageListView::GetStackedNotifications() const {
return message_center_view_->GetStackedNotifications();
}
std::vector<std::string>
UnifiedMessageListView::GetNonVisibleNotificationIdsInViewHierarchy() const {
return message_center_view_->GetNonVisibleNotificationIdsInViewHierarchy();
}
// static
const UnifiedMessageListView::MessageViewContainer*
UnifiedMessageListView::AsMVC(const views::View* v) {
return static_cast<const MessageViewContainer*>(v);
}
// static
UnifiedMessageListView::MessageViewContainer* UnifiedMessageListView::AsMVC(
views::View* v) {
return static_cast<MessageViewContainer*>(v);
}
const UnifiedMessageListView::MessageViewContainer*
UnifiedMessageListView::GetNotificationById(const std::string& id) const {
const auto i = std::find_if(
children().cbegin(), children().cend(),
[id](const auto* v) { return AsMVC(v)->GetNotificationId() == id; });
return (i == children().cend()) ? nullptr : AsMVC(*i);
}
UnifiedMessageListView::MessageViewContainer*
UnifiedMessageListView::GetNextRemovableNotification() {
const auto i =
std::find_if(children().cbegin(), children().cend(),
[](const auto* v) { return !AsMVC(v)->IsPinned(); });
return (i == children().cend()) ? nullptr : AsMVC(*i);
}
void UnifiedMessageListView::CollapseAllNotifications() {
base::AutoReset<bool> auto_reset(&ignore_size_change_, true);
for (auto* child : children())
AsMVC(child)->Collapse();
}
void UnifiedMessageListView::UpdateBorders(bool force_update) {
// We do not need individual notifications to have rounded corners
// on the borders with the new UI. This is because the entire
// scroll view has rounded corners now.
if (is_notifications_refresh_enabled_)
return;
// The top notification is drawn with rounded corners when the stacking bar is
// not shown.
bool is_top = state_ != State::MOVE_DOWN;
if (!is_notifications_refresh_enabled_)
is_top = is_top && children().size() == 1;
for (auto* child : children()) {
AsMVC(child)->UpdateBorder(is_top, child == children().back(),
force_update);
is_top = false;
}
}
void UnifiedMessageListView::UpdateBounds() {
int y = 0;
for (auto* child : children()) {
auto* view = AsMVC(child);
// Height is taken from preferred size, which is calculated based on the
// tween and animation state when animations are occurring. So views which
// are animating will provide the correct interpolated height here.
const int height = view->GetHeightForWidth(message_view_width_);
const int direction = view->GetSlideDirection();
if (y > 0 && is_notifications_refresh_enabled_)
y += kMessageListNotificationSpacing;
view->set_start_bounds(view->target_bounds());
view->set_target_bounds(view->is_removed()
? gfx::Rect(message_view_width_ * direction, y,
message_view_width_, height)
: gfx::Rect(0, y, message_view_width_, height));
y += height;
}
start_height_ = target_height_;
target_height_ = y;
}
void UnifiedMessageListView::ResetBounds() {
DeleteRemovedNotifications();
UpdateBounds();
state_ = State::IDLE;
if (animation_->is_animating())
animation_->End();
else
PreferredSizeChanged();
}
void UnifiedMessageListView::InterruptClearAll() {
if (state_ != State::CLEAR_ALL_STACKED && state_ != State::CLEAR_ALL_VISIBLE)
return;
for (auto* child : children()) {
auto* view = AsMVC(child);
if (!view->IsPinned())
view->set_is_removed();
}
DeleteRemovedNotifications();
}
void UnifiedMessageListView::DeleteRemovedNotifications() {
DCHECK(model_);
views::View::Views removed_views;
std::copy_if(children().cbegin(), children().cend(),
std::back_inserter(removed_views),
[](const auto* v) { return AsMVC(v)->is_removed(); });
{
base::AutoReset<bool> auto_reset(&is_deleting_removed_notifications_, true);
for (auto* view : removed_views) {
model_->RemoveNotificationExpanded(AsMVC(view)->GetNotificationId());
delete view;
}
}
UpdateBorders(/*force_update=*/false);
}
void UnifiedMessageListView::StartAnimation() {
DCHECK_NE(state_, State::IDLE);
base::TimeDelta animation_duration;
switch (state_) {
case State::IDLE:
break;
case State::MOVE_DOWN:
SetupThroughputTrackerForAnimationSmoothness(
GetWidget(), throughput_tracker_,
kMoveDownAnimationSmoothnessHistogramName);
animation_duration = kClosingAnimationDuration;
break;
case State::CLEAR_ALL_STACKED:
SetupThroughputTrackerForAnimationSmoothness(
GetWidget(), throughput_tracker_,
kClearAllStackedAnimationSmoothnessHistogramName);
animation_duration = kClearAllStackedAnimationDuration;
break;
case State::CLEAR_ALL_VISIBLE:
SetupThroughputTrackerForAnimationSmoothness(
GetWidget(), throughput_tracker_,
kClearAllVisibleAnimationSmoothnessHistogramName);
animation_duration = kClearAllVisibleAnimationDuration;
break;
case State::EXPAND_OR_COLLAPSE:
SetupThroughputTrackerForAnimationSmoothness(
GetWidget(), throughput_tracker_,
kExpandOrCollapseAnimationSmoothnessHistogramName);
DCHECK(expand_or_collapsing_container_);
animation_duration =
expand_or_collapsing_container_
? expand_or_collapsing_container_->GetBoundsAnimationDuration()
: base::Milliseconds(
kLargeImageExpandAndCollapseAnimationDuration);
break;
}
animation_->SetDuration(animation_duration);
animation_->Start();
}
void UnifiedMessageListView::UpdateClearAllAnimation() {
DCHECK(state_ == State::CLEAR_ALL_STACKED ||
state_ == State::CLEAR_ALL_VISIBLE);
auto* view = GetNextRemovableNotification();
if (view)
view->set_is_removed();
if (state_ == State::CLEAR_ALL_STACKED) {
const auto non_visible_notification_ids =
GetNonVisibleNotificationIdsInViewHierarchy();
if (view && non_visible_notification_ids.size() > 0) {
// Immediately remove all notifications that are outside of the scrollable
// window.
for (const auto& id : non_visible_notification_ids) {
auto* message_view_container = GetNotificationById(id);
if (message_view_container)
message_view_container->set_is_removed();
}
DeleteRemovedNotifications();
UpdateBounds();
start_height_ = target_height_;
for (auto* child : children()) {
auto* view = AsMVC(child);
view->set_start_bounds(view->target_bounds());
}
PreferredSizeChanged();
} else {
state_ = State::CLEAR_ALL_VISIBLE;
}
}
if (state_ == State::CLEAR_ALL_VISIBLE) {
UpdateBounds();
if (view || start_height_ != target_height_)
state_ = State::CLEAR_ALL_VISIBLE;
else
state_ = State::IDLE;
}
}
double UnifiedMessageListView::GetCurrentValue() const {
gfx::Tween::Type tween;
switch (state_) {
case State::IDLE:
// No animations are used for State::IDLE.
NOTREACHED();
tween = gfx::Tween::LINEAR;
break;
case State::CLEAR_ALL_STACKED:
case State::MOVE_DOWN:
tween = gfx::Tween::FAST_OUT_SLOW_IN;
break;
case State::CLEAR_ALL_VISIBLE:
tween = gfx::Tween::EASE_IN;
break;
case State::EXPAND_OR_COLLAPSE:
tween = gfx::Tween::FAST_OUT_SLOW_IN_3;
break;
}
return gfx::Tween::CalculateValue(tween, animation_->GetCurrentValue());
}
} // namespace ash