blob: 0358f142f4c0bad21382941ac4c941c78e816020 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/message_center/views/message_popup_collection.h"
#include <algorithm>
#include <vector>
#include "base/containers/adapters.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/ranges/algorithm.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/gfx/animation/tween.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_types.h"
#include "ui/message_center/notification_view_controller.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/views/message_popup_view.h"
#include "ui/message_center/views/message_view.h"
#include "ui/message_center/views/notification_view.h"
#include "ui/views/animation/animation_builder.h"
namespace message_center {
namespace {
// Animation duration for kFadeIn and kFadeOut.
constexpr base::TimeDelta kFadeInFadeOutDuration = base::Milliseconds(200);
// Animation duration for kMoveDown.
constexpr base::TimeDelta kMoveDownDuration = base::Milliseconds(120);
} // namespace
MessagePopupCollection::PopupItem::PopupItem() = default;
MessagePopupCollection::PopupItem::PopupItem(PopupItem&& other) = default;
MessagePopupCollection::PopupItem& MessagePopupCollection::PopupItem::operator=(
PopupItem&& other) = default;
MessagePopupCollection::PopupItem::~PopupItem() = default;
MessagePopupCollection::MessagePopupCollection()
: animation_(std::make_unique<gfx::LinearAnimation>(this)),
weak_ptr_factory_(this) {
message_center_observation_.Observe(MessageCenter::Get());
}
MessagePopupCollection::~MessagePopupCollection() {
// Ignore calls to update which can cause crashes.
is_updating_ = true;
for (auto& item : popup_items_) {
ClosePopupItem(item);
}
}
void MessagePopupCollection::Update() {
if (is_updating_)
return;
base::AutoReset<bool> reset(&is_updating_, true);
RemoveClosedPopupItems();
if (MessageCenter::Get()->IsMessageCenterVisible()) {
CloseAllPopupsNow();
return;
}
if (animation_->is_animating()) {
UpdateByAnimation();
return;
}
if (state_ != State::kIdle)
TransitionFromAnimation();
if (state_ == State::kIdle)
TransitionToAnimation();
UpdatePopupTimers();
if (state_ != State::kIdle) {
// If not in kIdle state, start animation.
base::TimeDelta animation_duration;
if (state_ == State::kMoveDown) {
animation_duration = kMoveDownDuration;
} else {
animation_duration = kFadeInFadeOutDuration;
}
animation_->SetDuration(
animation_duration *
ui::ScopedAnimationDurationScaleMode::duration_multiplier());
animation_->Start();
AnimationStarted();
UpdateByAnimation();
}
DCHECK(state_ == State::kIdle || animation_->is_animating());
}
void MessagePopupCollection::ResetBounds() {
if (is_updating_)
return;
{
base::AutoReset<bool> reset(&is_updating_, true);
RemoveClosedPopupItems();
state_ = State::kIdle;
animation_->End();
CalculateAndUpdateBounds();
// Remove popups that are no longer in work area.
ClosePopupsOutsideWorkArea();
// Reset bounds and opacity of popups.
for (auto& item : popup_items_) {
item.popup->SetPopupBounds(item.bounds);
item.popup->SetOpacity(1.0);
}
}
// Restart animation for FADE_OUT.
Update();
}
void MessagePopupCollection::NotifyPopupResized() {
resize_requested_ = true;
Update();
}
void MessagePopupCollection::NotifyPopupClosed(MessagePopupView* popup) {
CloseAndRemovePopupFromPopupItem(popup);
}
void MessagePopupCollection::AnimateResize() {
CalculateAndUpdateBounds();
views::AnimationBuilder animation_builder;
for (auto& popup : popup_items_) {
auto target_bounds = gfx::Rect(
popup.popup->GetWidget()->GetLayer()->bounds().x(), popup.bounds.y(),
popup.bounds.width(), popup.bounds.height());
animation_builder.Once()
.SetDuration(base::Milliseconds(kNotificationResizeAnimationDurationMs))
.SetBounds(popup.popup->GetWidget()->GetLayer(), target_bounds,
gfx::Tween::EASE_OUT);
}
}
MessageView* MessagePopupCollection::GetMessageViewForNotificationId(
const std::string& notification_id) {
auto it = base::ranges::find_if(popup_items_, [&](const auto& child) {
// Exit early if the popup ptr has been set to nullptr by
// `NotifyPopupClosed` but has not been cleared from `popup_items_`.
if (!child.popup)
return false;
auto* widget = child.popup->GetWidget();
// Do not return popups that are in the process of closing, but have not
// yet been removed from `popup_items_`.
if (!widget || widget->IsClosed())
return false;
return child.popup->message_view()->notification_id() == notification_id;
});
if (it == popup_items_.end())
return nullptr;
return it->popup->message_view();
}
void MessagePopupCollection::ConvertNotificationViewToGroupedNotificationView(
const std::string& ungrouped_notification_id,
const std::string& new_grouped_notification_id) {
auto it = base::ranges::find(popup_items_, ungrouped_notification_id,
&PopupItem::id);
if (it == popup_items_.end())
return;
it->id = new_grouped_notification_id;
it->popup->message_view()->set_notification_id(new_grouped_notification_id);
}
void MessagePopupCollection::ConvertGroupedNotificationViewToNotificationView(
const std::string& grouped_notification_id,
const std::string& new_single_notification_id) {
auto it =
base::ranges::find(popup_items_, grouped_notification_id, &PopupItem::id);
if (it == popup_items_.end())
return;
it->id = new_single_notification_id;
it->popup->message_view()->set_notification_id(new_single_notification_id);
}
void MessagePopupCollection::OnChildNotificationViewUpdated(
const std::string& parent_notification_id,
const std::string& child_notification_id) {
auto* notification =
MessageCenter::Get()->FindNotificationById(child_notification_id);
if (!notification) {
return;
}
auto* parent_popup = GetPopupViewForNotificationID(parent_notification_id);
if (parent_popup) {
parent_popup->UpdateContentsForChildNotification(child_notification_id,
*notification);
}
}
void MessagePopupCollection::OnNotificationAdded(
const std::string& notification_id) {
// Should not call MessagePopupCollection::Update here. Because notification
// may be removed before animation which is triggered by the previous
// operation on MessagePopupCollection ends. As result, when a new
// notification with the same ID is created, calling
// MessagePopupCollection::Update will not update the popup's content. Then
// the new notification popup fails to show. (see https://crbug.com/921402)
OnNotificationUpdated(notification_id);
// Notify if the incoming notification is silent.
const Notification* notification =
message_center::MessageCenter::Get()->FindNotificationById(
notification_id);
if (notification && notification->priority() < DEFAULT_PRIORITY) {
NotifySilentNotification(notification->id());
}
}
void MessagePopupCollection::OnNotificationRemoved(
const std::string& notification_id,
bool by_user) {
Update();
}
void MessagePopupCollection::OnNotificationUpdated(
const std::string& notification_id) {
if (is_updating_)
return;
// Find Notification object with |notification_id|.
const auto& notifications = GetPopupNotifications();
auto it = notifications.begin();
while (it != notifications.end()) {
if ((*it)->id() == notification_id)
break;
++it;
}
if (it == notifications.end()) {
// If not found, probably |notification_id| is removed from popups by
// timeout.
Update();
return;
}
{
base::AutoReset<bool> reset(&is_updating_, true);
RemoveClosedPopupItems();
// Update contents of the notification.
for (const auto& item : popup_items_) {
if (item.id == notification_id) {
item.popup->UpdateContents(**it);
break;
}
}
}
Update();
}
void MessagePopupCollection::OnCenterVisibilityChanged(Visibility visibility) {
Update();
}
void MessagePopupCollection::OnBlockingStateChanged(
NotificationBlocker* blocker) {
Update();
}
void MessagePopupCollection::AnimationEnded(const gfx::Animation* animation) {
Update();
AnimationFinished();
}
void MessagePopupCollection::AnimationProgressed(
const gfx::Animation* animation) {
Update();
}
void MessagePopupCollection::AnimationCanceled(
const gfx::Animation* animation) {
Update();
}
MessagePopupView* MessagePopupCollection::GetPopupViewForNotificationID(
const std::string& notification_id) {
for (const auto& item : popup_items_) {
if (item.id == notification_id)
return item.popup;
}
return nullptr;
}
size_t MessagePopupCollection::GetPopupItemsCount() {
return popup_items_.size();
}
MessagePopupView* MessagePopupCollection::CreatePopup(
const Notification& notification) {
bool a11_feedback_on_init =
notification.rich_notification_data()
.should_make_spoken_feedback_for_popup_updates;
return new MessagePopupView(new NotificationView(notification), this,
a11_feedback_on_init);
}
bool MessagePopupCollection::IsNextEdgeOutsideWorkArea(
const PopupItem& item) const {
const int next_edge = GetNextEdge(item);
const gfx::Rect work_area = GetWorkArea();
return IsTopDown() ? next_edge > work_area.bottom()
: next_edge < work_area.y();
}
void MessagePopupCollection::ClosePopupItem(PopupItem& item) {
if (MessagePopupView* popup = item.popup) {
popup->Close();
// Re-check item.popup since the Close() call may have deleted it.
if (popup == item.popup) {
if (!popup->view_added_to_widget()) {
// Take ownership and delete when leaving scope.
auto owned_popup = base::WrapUnique(popup);
// This doesn't delete the delegate, but does ensure notifications about
// it are still sent.
owned_popup->DeleteDelegate();
CloseAndRemovePopupFromPopupItem(owned_popup.get(), true);
}
}
if (item.widget) {
item.widget.reset();
}
}
}
void MessagePopupCollection::MoveDownPopups() {
CalculateAndUpdateBounds();
for (auto& item : popup_items_) {
item.is_animating = true;
}
}
void MessagePopupCollection::RestartPopupTimers() {
MessageCenter::Get()->RestartPopupTimers();
}
void MessagePopupCollection::PausePopupTimers() {
MessageCenter::Get()->PausePopupTimers();
}
void MessagePopupCollection::CloseAllPopupsNow() {
for (auto& item : popup_items_) {
// A popup might have already been removed when this is called.
if (!item.popup) {
continue;
}
item.is_animating = true;
// Mark the popup as shown so that the popup item will not re-appear after
// any subsequent calls to `GetPopupNotifications()`.
MessageCenter::Get()->MarkSinglePopupAsShown(
item.id, /*mark_notification_as_read=*/true);
}
CloseAnimatingPopups();
state_ = State::kIdle;
animation_->End();
}
void MessagePopupCollection::TransitionFromAnimation() {
DCHECK_NE(state_, State::kIdle);
DCHECK(!animation_->is_animating());
// The animation of type |state_| is now finished.
UpdateByAnimation();
// If kFadeOut animation is finished, remove the animated popup.
if (state_ == State::kFadeOut) {
CloseAnimatingPopups();
}
if (state_ == State::kFadeIn || state_ == State::kMoveDown ||
(state_ == State::kFadeOut && popup_items_.empty())) {
// If the animation is finished, transition to kIdle.
state_ = State::kIdle;
} else if (state_ == State::kFadeOut && !popup_items_.empty()) {
if (HasAddedPopup()) {
CollapseAllPopups();
}
// If kFadeOut animation is finished and we still have remaining popups,
// we have to kMoveDown them.
// If we're going to add a new popup after this kMoveDown, do the collapse
// animation at the same time. Otherwise it will take another kMoveDown.
state_ = State::kMoveDown;
MoveDownPopups();
}
}
void MessagePopupCollection::TransitionToAnimation() {
DCHECK_EQ(state_, State::kIdle);
DCHECK(!animation_->is_animating());
if (HasRemovedPopup()) {
MarkRemovedPopup();
if (CloseTransparentPopups()) {
// If the popup is already transparent, skip kFadeOut.
state_ = State::kMoveDown;
MoveDownPopups();
} else {
state_ = State::kFadeOut;
}
return;
}
if (HasAddedPopup()) {
if (CollapseAllPopups()) {
// If we had existing popups that weren't collapsed, first show collapsing
// animation.
state_ = State::kMoveDown;
MoveDownPopups();
return;
} else if (AddPopup()) {
// A popup is actually added. Show kFadein animation.
state_ = State::kFadeIn;
return;
}
}
if (resize_requested_) {
// Resize is requested e.g. a user manually expanded notification.
resize_requested_ = false;
state_ = State::kMoveDown;
MoveDownPopups();
// This function may be called by a child MessageView when a notification is
// expanded by the user. Deleting the pop-up should be delayed so we are
// out of the child view's call stack. See crbug.com/957033.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&MessagePopupCollection::ClosePopupsOutsideWorkArea,
weak_ptr_factory_.GetWeakPtr()));
return;
}
}
void MessagePopupCollection::UpdatePopupTimers() {
if (state_ == State::kIdle) {
if (IsAnyPopupHovered() || IsAnyPopupFocused()) {
// If any popup is hovered or focused, pause popup timer.
PausePopupTimers();
} else {
// If in kIdle state, restart popup timer.
RestartPopupTimers();
}
} else {
// If not in kIdle state, pause popup timer.
PausePopupTimers();
}
}
void MessagePopupCollection::CalculateAndUpdateBounds() {
int base = GetBaseline();
int popup_bounds_origin_x = 0;
int popup_bounds_origin_y = 0;
int popup_bounds_height = 0;
if (IsTopDown()) {
popup_bounds_origin_y = base;
}
int notification_width = GetNotificationWidth();
for (size_t i = 0; i < popup_items_.size(); ++i) {
gfx::Size preferred_size(
notification_width,
GetPopupItem(i)->popup->GetHeightForWidth(notification_width));
int origin_x = GetPopupOriginX(gfx::Rect(preferred_size));
popup_bounds_origin_x = origin_x;
int origin_y = base;
if (!IsTopDown())
origin_y -= preferred_size.height();
GetPopupItem(i)->start_bounds = GetPopupItem(i)->bounds;
GetPopupItem(i)->bounds =
gfx::Rect(gfx::Point(origin_x, origin_y), preferred_size);
const int delta = preferred_size.height() + kMarginBetweenPopups;
if (IsTopDown())
base += delta;
else
base -= delta;
popup_bounds_height += delta;
}
if (!IsTopDown()) {
popup_bounds_origin_y = base + kMarginBetweenPopups;
}
int old_popup_collection_height = popup_collection_bounds_.height();
popup_collection_bounds_ =
gfx::Rect(popup_bounds_origin_x, popup_bounds_origin_y,
notification_width, popup_bounds_height - kMarginBetweenPopups);
if (old_popup_collection_height != popup_collection_bounds_.height()) {
NotifyPopupCollectionHeightChanged();
}
}
void MessagePopupCollection::UpdateByAnimation() {
DCHECK_NE(state_, State::kIdle);
for (auto& item : popup_items_) {
if (!item.is_animating)
continue;
double value = gfx::Tween::CalculateValue(
state_ == State::kFadeOut ? gfx::Tween::EASE_IN : gfx::Tween::EASE_OUT,
animation_->GetCurrentValue());
if (state_ == State::kFadeIn)
item.popup->SetOpacity(gfx::Tween::FloatValueBetween(value, 0.0f, 1.0f));
else if (state_ == State::kFadeOut)
item.popup->SetOpacity(gfx::Tween::FloatValueBetween(value, 1.0f, 0.0f));
if (state_ == State::kFadeIn || state_ == State::kMoveDown) {
item.popup->SetPopupBounds(
gfx::Tween::RectValueBetween(value, item.start_bounds, item.bounds));
}
}
}
std::vector<Notification*> MessagePopupCollection::GetPopupNotifications()
const {
std::vector<Notification*> result;
for (auto* notification : MessageCenter::Get()->GetPopupNotifications()) {
// Disables popup of custom notification on non-primary displays, since
// currently custom notification supports only on one display at the same
// time.
// TODO(yoshiki): Support custom popup notification on multiple display
// (https://crbug.com/715370).
if (!IsPrimaryDisplayForNotification() &&
notification->type() == NOTIFICATION_TYPE_CUSTOM) {
continue;
}
if (notification->group_child())
continue;
if (BlockForMixedFullscreen(*notification))
continue;
result.emplace_back(notification);
}
return result;
}
bool MessagePopupCollection::AddPopup() {
std::set<std::string> existing_ids;
for (const auto& item : popup_items_)
existing_ids.insert(item.id);
auto notifications = GetPopupNotifications();
Notification* new_notification = nullptr;
// Reverse iterating because notifications are in reverse chronological order.
for (Notification* notification : base::Reversed(notifications)) {
if (!existing_ids.count(notification->id())) {
new_notification = notification;
break;
}
}
if (!new_notification)
return false;
// Reset animation flags of existing popups.
for (auto& item : popup_items_) {
item.is_animating = false;
}
if (new_notification->group_child())
return false;
{
PopupItem item;
item.id = new_notification->id();
item.is_animating = true;
item.popup = CreatePopup(*new_notification);
if (IsNextEdgeOutsideWorkArea(item)) {
ClosePopupItem(item);
return false;
}
item.widget = item.popup->Show();
popup_items_.push_back(std::move(item));
NotifyPopupAdded(popup_items_.back().popup);
}
MessageCenter::Get()->DisplayedNotification(new_notification->id(),
DISPLAY_SOURCE_POPUP);
CalculateAndUpdateBounds();
// We might remove all popup items after update bounds.
// TODO(b/302172146): Remove this check once we have the long-term solution
// for notifier collision.
if (!popup_items_.empty()) {
auto& item = popup_items_.back();
item.start_bounds = item.bounds;
item.start_bounds +=
gfx::Vector2d((IsFromLeft() ? -1 : 1) * item.bounds.width(), 0);
}
return true;
}
void MessagePopupCollection::MarkRemovedPopup() {
std::set<std::string> existing_ids;
for (Notification* notification : GetPopupNotifications()) {
existing_ids.insert(notification->id());
}
for (auto& item : popup_items_) {
bool removing = !existing_ids.count(item.id);
item.is_animating = removing;
if (removing)
NotifyPopupRemoved(item.id);
}
}
int MessagePopupCollection::GetNextEdge(const PopupItem& item) const {
const int delta = item.popup->GetHeightForWidth(GetNotificationWidth()) +
kMarginBetweenPopups;
int base = 0;
if (popup_items_.empty()) {
base = GetBaseline();
} else {
base = IsTopDown() ? popup_items_.back().bounds.bottom()
: popup_items_.back().bounds.y();
}
return IsTopDown() ? base + delta : base - delta;
}
void MessagePopupCollection::CloseAnimatingPopups() {
for (auto& item : popup_items_) {
if (!item.is_animating)
continue;
ClosePopupItem(item);
}
RemoveClosedPopupItems();
}
bool MessagePopupCollection::CloseTransparentPopups() {
bool removed = false;
for (auto& item : popup_items_) {
if (item.popup->GetOpacity() > 0.0)
continue;
ClosePopupItem(item);
removed = true;
}
RemoveClosedPopupItems();
return removed;
}
void MessagePopupCollection::ClosePopupsOutsideWorkArea() {
const gfx::Rect work_area = GetWorkArea();
for (auto& item : popup_items_) {
if (work_area.Contains(item.bounds))
continue;
ClosePopupItem(item);
}
RemoveClosedPopupItems();
}
void MessagePopupCollection::RemoveClosedPopupItems() {
std::erase_if(popup_items_, [](const auto& item) { return !item.popup; });
}
void MessagePopupCollection::CloseAndRemovePopupFromPopupItem(
MessagePopupView* popup,
bool remove_only) {
for (auto& item : popup_items_) {
if (item.popup && item.popup == popup) {
if (!remove_only) {
popup->Close();
}
item.popup = nullptr;
}
}
}
bool MessagePopupCollection::CollapseAllPopups() {
bool changed = false;
int notification_width = GetNotificationWidth();
for (auto& item : popup_items_) {
int old_height = item.popup->GetHeightForWidth(notification_width);
item.popup->AutoCollapse();
int new_height = item.popup->GetHeightForWidth(notification_width);
if (old_height != new_height)
changed = true;
}
resize_requested_ = false;
return changed;
}
bool MessagePopupCollection::HasAddedPopup() const {
std::set<std::string> existing_ids;
for (const auto& item : popup_items_)
existing_ids.insert(item.id);
for (Notification* notification : GetPopupNotifications()) {
if (!existing_ids.count(notification->id())) {
// A new popup is not added for a group child if it's parent
// notification has an existing popup.
if (notification->group_child()) {
auto* parent_notification =
MessageCenter::Get()->FindParentNotification(notification);
return !existing_ids.count(parent_notification->id());
}
return true;
}
}
return false;
}
bool MessagePopupCollection::HasRemovedPopup() const {
std::set<std::string> existing_ids;
for (Notification* notification : GetPopupNotifications()) {
existing_ids.insert(notification->id());
}
for (const auto& item : popup_items_) {
if (!existing_ids.count(item.id))
return true;
}
return false;
}
bool MessagePopupCollection::IsAnyPopupHovered() const {
for (const auto& item : popup_items_) {
if (item.popup->is_hovered())
return true;
}
return false;
}
bool MessagePopupCollection::IsAnyPopupFocused() const {
for (const auto& item : popup_items_) {
if (item.popup->is_focused())
return true;
}
return false;
}
MessagePopupCollection::PopupItem* MessagePopupCollection::GetPopupItem(
size_t index_from_top) {
DCHECK_LT(index_from_top, popup_items_.size());
return &popup_items_[index_from_top];
}
} // namespace message_center