#include "ash/system/notification_center/stacked_notification_bar.h"
#include <algorithm>
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/pill_button.h"
#include "ash/style/style_util.h"
#include "ash/style/typography.h"
#include "ash/system/notification_center/message_center_constants.h"
#include "ash/system/notification_center/views/notification_center_view.h"
#include "ash/system/tray/tray_constants.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/compositor/layer_animator.h"
#include "ui/gfx/interpolated_transform.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/vector_icons.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
namespace ash {
namespace {
class StackingBarLabelButton : public PillButton {
METADATA_HEADER(StackingBarLabelButton, PillButton)
public:
StackingBarLabelButton(PressedCallback callback,
const std::u16string& text,
NotificationCenterView* notification_center_view)
: PillButton(std::move(callback),
text,
PillButton::Type::kFloatingWithoutIcon,
nullptr,
kNotificationPillButtonHorizontalSpacing) {
SetEnabled(false);
StyleUtil::SetUpInkDropForButton(this, gfx::Insets(),
true,
true);
}
StackingBarLabelButton(const StackingBarLabelButton&) = delete;
StackingBarLabelButton& operator=(const StackingBarLabelButton&) = delete;
~StackingBarLabelButton() override = default;
};
BEGIN_METADATA(StackingBarLabelButton)
END_METADATA
}
class StackedNotificationBar::StackedNotificationBarIcon
: public views::ImageView,
public ui::LayerAnimationObserver {
METADATA_HEADER(StackedNotificationBarIcon, views::ImageView)
public:
explicit StackedNotificationBarIcon(const std::string& id) : id_(id) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
}
~StackedNotificationBarIcon() override {
StopObserving();
if (is_animating_out())
layer()->GetAnimator()->StopAnimating();
}
void OnThemeChanged() override {
views::ImageView::OnThemeChanged();
auto* notification =
message_center::MessageCenter::Get()->FindVisibleNotificationById(id_);
if (!notification)
return;
SkColor accent_color =
GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurface);
gfx::Image masked_small_icon = notification->GenerateMaskedSmallIcon(
kStackedNotificationIconSize, accent_color, SK_ColorTRANSPARENT,
accent_color);
if (masked_small_icon.IsEmpty()) {
SetImage(ui::ImageModel::FromVectorIcon(message_center::kProductIcon,
accent_color,
kStackedNotificationIconSize));
} else {
SetImage(ui::ImageModel::FromImage(masked_small_icon));
}
}
void AnimateIn() {
DCHECK(!is_animating_out());
std::unique_ptr<ui::InterpolatedTransform> scale =
std::make_unique<ui::InterpolatedScale>(
gfx::Point3F(kNotificationIconAnimationScaleFactor,
kNotificationIconAnimationScaleFactor, 1),
gfx::Point3F(1, 1, 1));
std::unique_ptr<ui::InterpolatedTransform> scale_about_pivot =
std::make_unique<ui::InterpolatedTransformAboutPivot>(
GetLocalBounds().CenterPoint(), std::move(scale));
scale_about_pivot->SetChild(std::make_unique<ui::InterpolatedTranslation>(
gfx::PointF(0, kNotificationIconAnimationLowPosition),
gfx::PointF(0, kNotificationIconAnimationHighPosition)));
std::unique_ptr<ui::LayerAnimationElement> scale_and_move_up =
ui::LayerAnimationElement::CreateInterpolatedTransformElement(
std::move(scale_about_pivot),
base::Milliseconds(kNotificationIconAnimationUpDurationMs));
scale_and_move_up->set_tween_type(gfx::Tween::EASE_IN);
std::unique_ptr<ui::LayerAnimationElement> move_down =
ui::LayerAnimationElement::CreateInterpolatedTransformElement(
std::make_unique<ui::InterpolatedTranslation>(
gfx::PointF(0, kNotificationIconAnimationHighPosition),
gfx::PointF(0, 0)),
base::Milliseconds(kNotificationIconAnimationDownDurationMs));
std::unique_ptr<ui::LayerAnimationSequence> sequence =
std::make_unique<ui::LayerAnimationSequence>();
sequence->AddElement(std::move(scale_and_move_up));
sequence->AddElement(std::move(move_down));
layer()->GetAnimator()->StartAnimation(sequence.release());
}
using AnimationCompleteCallback = base::OnceCallback<void(views::View*)>;
void AnimateOut(AnimationCompleteCallback animation_complete_callback) {
DCHECK(animation_complete_callback_.is_null());
animation_complete_callback_ = std::move(animation_complete_callback);
layer()->GetAnimator()->StopAnimating();
std::unique_ptr<ui::InterpolatedTransform> scale =
std::make_unique<ui::InterpolatedScale>(
gfx::Point3F(1, 1, 1),
gfx::Point3F(kNotificationIconAnimationScaleFactor,
kNotificationIconAnimationScaleFactor, 1));
std::unique_ptr<ui::InterpolatedTransform> scale_about_pivot =
std::make_unique<ui::InterpolatedTransformAboutPivot>(
gfx::Point(bounds().width() * 0.5, bounds().height() * 0.5),
std::move(scale));
scale_about_pivot->SetChild(std::make_unique<ui::InterpolatedTranslation>(
gfx::PointF(0, 0),
gfx::PointF(0, kNotificationIconAnimationLowPosition)));
std::unique_ptr<ui::LayerAnimationElement> scale_and_move_down =
ui::LayerAnimationElement::CreateInterpolatedTransformElement(
std::move(scale_about_pivot),
base::Milliseconds(kNotificationIconAnimationOutDurationMs));
scale_and_move_down->set_tween_type(gfx::Tween::EASE_IN);
std::unique_ptr<ui::LayerAnimationSequence> sequence =
std::make_unique<ui::LayerAnimationSequence>();
sequence->AddElement(std::move(scale_and_move_down));
sequence->AddObserver(this);
set_animating_out(true);
layer()->GetAnimator()->StartAnimation(sequence.release());
}
void OnLayerAnimationEnded(ui::LayerAnimationSequence* sequence) override {
set_animating_out(false);
std::move(animation_complete_callback_).Run(this);
}
void OnLayerAnimationAborted(ui::LayerAnimationSequence* sequence) override {}
void OnLayerAnimationScheduled(
ui::LayerAnimationSequence* sequence) override {}
const std::string& id() const { return id_; }
bool is_animating_out() const { return animating_out_; }
void set_animating_out(bool animating_out) { animating_out_ = animating_out; }
private:
std::string id_;
bool animating_out_ = false;
AnimationCompleteCallback animation_complete_callback_;
};
BEGIN_METADATA(StackedNotificationBar, StackedNotificationBarIcon)
END_METADATA
StackedNotificationBar::StackedNotificationBar(
NotificationCenterView* notification_center_view)
: notification_center_view_(notification_center_view),
notification_icons_container_(
AddChildView(std::make_unique<views::View>())),
count_label_(AddChildView(std::make_unique<views::Label>())),
spacer_(AddChildView(std::make_unique<views::View>())),
clear_all_button_(AddChildView(std::make_unique<StackingBarLabelButton>(
base::BindRepeating(&NotificationCenterView::ClearAllNotifications,
base::Unretained(notification_center_view_)),
l10n_util::GetStringUTF16(
IDS_ASH_MESSAGE_CENTER_CLEAR_ALL_BUTTON_LABEL),
notification_center_view))),
layout_manager_(SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
kNotificationBarPadding))) {
layout_manager_->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStretch);
notification_icons_container_->SetLayoutManager(
std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
kStackedNotificationIconsContainerPadding,
kStackedNotificationBarIconSpacing));
message_center::MessageCenter::Get()->AddObserver(this);
count_label_->SetEnabledColor(cros_tokens::kCrosSysOnSurface);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosAnnotation1,
*count_label_);
layout_manager_->SetFlexForView(spacer_, 1);
clear_all_button_->SetTooltipText(l10n_util::GetStringUTF16(
IDS_ASH_MESSAGE_CENTER_CLEAR_ALL_BUTTON_TOOLTIP));
}
StackedNotificationBar::~StackedNotificationBar() {
if (message_center::MessageCenter::Get())
message_center::MessageCenter::Get()->RemoveObserver(this);
}
bool StackedNotificationBar::Update(
int total_notification_count,
int pinned_notification_count,
std::vector<raw_ptr<message_center::Notification, VectorExperimental>>
stacked_notifications) {
int stacked_notification_count = stacked_notifications.size();
if (total_notification_count == total_notification_count_ &&
pinned_notification_count == pinned_notification_count_ &&
stacked_notification_count == stacked_notification_count_) {
return false;
}
total_notification_count_ = total_notification_count;
pinned_notification_count_ = pinned_notification_count;
UpdateStackedNotifications(stacked_notifications);
const int unpinned_count =
total_notification_count_ - pinned_notification_count_;
auto tooltip = l10n_util::GetStringFUTF16Int(
IDS_ASH_MESSAGE_CENTER_STACKING_BAR_CLEAR_ALL_BUTTON_TOOLTIP,
unpinned_count);
clear_all_button_->SetTooltipText(tooltip);
clear_all_button_->GetViewAccessibility().SetName(tooltip);
clear_all_button_->SetEnabled(unpinned_count > 0);
return true;
}
void StackedNotificationBar::AddNotificationIcon(
message_center::Notification* notification,
bool at_front) {
if (at_front)
notification_icons_container_->AddChildViewAt(
std::make_unique<StackedNotificationBarIcon>(notification->id()), 0);
else
notification_icons_container_->AddChildView(
std::make_unique<StackedNotificationBarIcon>(notification->id()));
}
void StackedNotificationBar::OnIconAnimatedOut(std::string notification_id,
views::View* icon) {
delete icon;
auto* notification =
message_center::MessageCenter::Get()->FindVisibleNotificationById(
notification_id);
if (notification)
AddNotificationIcon(notification, false);
DeprecatedLayoutImmediately();
}
StackedNotificationBar::StackedNotificationBarIcon*
StackedNotificationBar::GetFrontIcon(bool animating_out) {
const auto i = std::ranges::find(
notification_icons_container_->children(), animating_out,
[](const views::View* v) {
return static_cast<const StackedNotificationBarIcon*>(v)
->is_animating_out();
});
return (i == notification_icons_container_->children().cend()
? nullptr
: static_cast<StackedNotificationBarIcon*>(*i));
}
const StackedNotificationBar::StackedNotificationBarIcon*
StackedNotificationBar::GetIconFromId(const std::string& id) const {
for (views::View* v : notification_icons_container_->children()) {
const StackedNotificationBarIcon* icon =
static_cast<const StackedNotificationBarIcon*>(v);
if (icon->id() == id)
return icon;
}
return nullptr;
}
void StackedNotificationBar::ShiftIconsLeft(
std::vector<raw_ptr<message_center::Notification, VectorExperimental>>
stacked_notifications) {
auto* front_animating_out_icon = GetFrontIcon(true);
bool is_already_animating_a_left_shift = front_animating_out_icon != nullptr;
if (is_already_animating_a_left_shift) {
front_animating_out_icon->layer()->GetAnimator()->StopAnimating();
}
int stacked_notification_count = stacked_notifications.size();
int removed_icons_count =
std::min(stacked_notification_count_ - stacked_notification_count,
kStackedNotificationBarMaxIcons);
stacked_notification_count_ = stacked_notification_count;
int backfill_start = kStackedNotificationBarMaxIcons - removed_icons_count;
int backfill_end =
std::min(kStackedNotificationBarMaxIcons, stacked_notification_count);
const bool will_animate = removed_icons_count == 1;
if (will_animate) {
auto* icon = GetFrontIcon(false);
if (icon) {
message_center::Notification* next_notification =
backfill_start < backfill_end ? stacked_notifications[backfill_start]
: nullptr;
icon->AnimateOut(base::BindOnce(
&StackedNotificationBar::OnIconAnimatedOut,
weak_ptr_factory_.GetWeakPtr(),
next_notification ? next_notification->id() : std::string()));
}
return;
}
for (int i = 0; i < removed_icons_count; i++) {
auto* icon = GetFrontIcon(false);
if (icon) {
delete icon;
}
}
for (int i = backfill_start; i < backfill_end; i++)
AddNotificationIcon(stacked_notifications[i], false );
}
void StackedNotificationBar::ShiftIconsRight(
std::vector<raw_ptr<message_center::Notification, VectorExperimental>>
stacked_notifications) {
int new_stacked_notification_count = stacked_notifications.size();
while (stacked_notification_count_ < new_stacked_notification_count) {
if (stacked_notification_count_ >= kStackedNotificationBarMaxIcons) {
delete notification_icons_container_->children().back();
}
AddNotificationIcon(stacked_notifications[new_stacked_notification_count -
stacked_notification_count_ - 1],
true );
++stacked_notification_count_;
}
auto* icon = GetFrontIcon(false);
if (icon)
icon->AnimateIn();
}
void StackedNotificationBar::UpdateStackedNotifications(
std::vector<raw_ptr<message_center::Notification, VectorExperimental>>
stacked_notifications) {
int stacked_notification_count = stacked_notifications.size();
int notification_overflow_count = 0;
if (stacked_notification_count_ > stacked_notification_count)
ShiftIconsLeft(stacked_notifications);
else if (stacked_notification_count_ < stacked_notification_count)
ShiftIconsRight(stacked_notifications);
notification_overflow_count = std::max(
stacked_notification_count_ - kStackedNotificationBarMaxIcons, 0);
if (notification_overflow_count > 0) {
count_label_->SetText(l10n_util::GetStringFUTF16Int(
IDS_ASH_MESSAGE_CENTER_HIDDEN_NOTIFICATION_COUNT_LABEL,
notification_overflow_count));
count_label_->SetVisible(true);
} else {
count_label_->SetVisible(false);
}
}
void StackedNotificationBar::OnNotificationAdded(const std::string& id) {
notification_icons_container_->RemoveAllChildViews();
stacked_notification_count_ = 0;
UpdateStackedNotifications(
notification_center_view_->GetStackedNotifications());
}
void StackedNotificationBar::OnNotificationRemoved(const std::string& id,
bool by_user) {
const StackedNotificationBarIcon* icon = GetIconFromId(id);
if (icon && !icon->is_animating_out()) {
delete icon;
stacked_notification_count_--;
}
}
void StackedNotificationBar::OnNotificationUpdated(const std::string& id) {}
BEGIN_METADATA(StackedNotificationBar)
END_METADATA
}