chromium/ash/system/notification_center/stacked_notification_bar.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/notification_center/stacked_notification_bar.h"

#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 "base/ranges/algorithm.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 {

// The label button in the stacked notification bar, used for the "Clear All"
// button.
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,
                   /*icon=*/nullptr,
                   kNotificationPillButtonHorizontalSpacing) {
    SetEnabled(false);
    StyleUtil::SetUpInkDropForButton(this, gfx::Insets(),
                                     /*highlight_on_hover=*/true,
                                     /*highlight_on_focus=*/true);
  }

  StackingBarLabelButton(const StackingBarLabelButton&) = delete;
  StackingBarLabelButton& operator=(const StackingBarLabelButton&) = delete;

  ~StackingBarLabelButton() override = default;
};

BEGIN_METADATA(StackingBarLabelButton)
END_METADATA

}  // namespace

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_);
    // The notification icon could be waiting to be cleaned up after the
    // notification removal animation completes.
    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(gfx::CreateVectorIcon(message_center::kProductIcon,
                                     kStackedNotificationIconSize,
                                     accent_color));
    } else {
      SetImage(masked_small_icon.AsImageSkia());
    }
  }

  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());
    // Note |this| may be deleted after this point.
  }

  // ui::LayerAnimationObserver:
  void OnLayerAnimationEnded(ui::LayerAnimationSequence* sequence) override {
    set_animating_out(false);
    std::move(animation_complete_callback_).Run(this);
    // Note |this| is deleted after this point.
  }

  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;

  // Used to notify the parent of animation completion. This is deleted after
  // the callback is run.
  // Registered in `AnimateOut()`.
  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_->SetEnabledColorId(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() {
  // The MessageCenter may be destroyed already during shutdown. See
  // crbug.com/946153.
  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);
  // This is only called when icons animate out, so never add icons to the
  // front.
  if (notification)
    AddNotificationIcon(notification, /*at_front=*/false);

  DeprecatedLayoutImmediately();
}

StackedNotificationBar::StackedNotificationBarIcon*
StackedNotificationBar::GetFrontIcon(bool animating_out) {
  const auto i = base::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(/*animating_out=*/true);
  bool is_already_animating_a_left_shift = front_animating_out_icon != nullptr;
  // If we need to animate a second icon, the scroll is faster than the icon can
  // animate out (this is possible with a very fast scroll), so immediately
  // finish that animation before starting a new one.
  if (is_already_animating_a_left_shift) {
    front_animating_out_icon->layer()->GetAnimator()->StopAnimating();
    // `front_animating_out_icon` is now deleted, and StackedNotificationBar has
    // been reloaded with another icon in the back.
  }

  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;

  // Remove required number of icons from the front.
  // Only animate if we're removing one icon.
  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(/*animating_out=*/false);
    if (icon) {
      // If there are notifications to backfill, do not add the
      // icon until the animation completes, this avoids a jumping overflow
      // label/icons and having more than 3 icons in the stack.
      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;
  }

  // No animation.
  for (int i = 0; i < removed_icons_count; i++) {
    auto* icon = GetFrontIcon(/*animating_out=*/false);
    if (icon) {
      delete icon;
    }
  }

  for (int i = backfill_start; i < backfill_end; i++)
    AddNotificationIcon(stacked_notifications[i], false /*at_front*/);
}

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) {
    // Remove icon from the back in case there is an overflow.
    if (stacked_notification_count_ >= kStackedNotificationBarMaxIcons) {
      delete notification_icons_container_->children().back();
    }
    // Add icon to the front.
    AddNotificationIcon(stacked_notifications[new_stacked_notification_count -
                                              stacked_notification_count_ - 1],
                        true /*at_front*/);
    ++stacked_notification_count_;
  }
  // Animate in the first stacked notification icon.
  auto* icon = GetFrontIcon(/*animating_out=*/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);

  // Update overflow count label
  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) {
  // Reset the stacked icons bar if a notification is added since we don't
  // know the position where it may have been added.
  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

}  // namespace ash