chromium/ash/system/notification_center/views/notification_center_view.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/views/notification_center_view.h"

#include <climits>
#include <memory>
#include <optional>

#include "ash/constants/ash_features.h"
#include "ash/system/notification_center/message_center_constants.h"
#include "ash/system/notification_center/stacked_notification_bar.h"
#include "ash/system/notification_center/views/message_center_scroll_bar.h"
#include "ash/system/notification_center/views/notification_list_view.h"
#include "ash/system/tray/tray_constants.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/views/message_view.h"
#include "ui/views/background.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

// Inset the top and the bottom of the scroll bar so it won't be clipped by
// rounded corners.
constexpr auto kScrollBarInsets = gfx::Insets::TLBR(16, 0, 16, 0);

}  // namespace

// TODO(https://b/307940238): Use `unique_ptrs` instead of `new`.
NotificationCenterView::NotificationCenterView()
    : notification_bar_(new StackedNotificationBar(this)),
      // TODO(crbug.com/1247455): Determine how to use ScrollWithLayers without
      // breaking ARC.
      scroller_(new views::ScrollView()),
      notification_list_view_(new NotificationListView(this)) {
  notification_list_view_tracker_.SetView(notification_list_view_);
  notification_list_view_tracker_.SetIsDeletingCallback(base::BindOnce(
      [](raw_ptr<NotificationListView>& notification_list_view) {
        notification_list_view = nullptr;
      },
      std::ref(notification_list_view_)));

  auto* scroll_bar = new MessageCenterScrollBar();
  scroll_bar->SetInsets(kScrollBarInsets);
  scroll_bar_ = scroll_bar;

  layout_manager_ = SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical,
      gfx::Insets(kMessageCenterPadding)));
}

NotificationCenterView::~NotificationCenterView() {
  scroller_->RemoveObserver(this);
}

void NotificationCenterView::Init() {
  CHECK(!features::IsNotificationCenterControllerEnabled());
  notification_list_view_->Init();
  AddChildViews();
}

void NotificationCenterView::Init(
    const std::vector<message_center::Notification*>& notifications) {
  CHECK(features::IsNotificationCenterControllerEnabled() &&
        !features::AreOngoingProcessesEnabled());
  notification_list_view_->Init(notifications);
  AddChildViews();
}

void NotificationCenterView::Init(
    const std::vector<message_center::Notification*>& unpinned_notifications,
    std::unique_ptr<views::View> pinned_notification_list_view) {
  CHECK(features::AreOngoingProcessesEnabled());
  notification_list_view_->Init(unpinned_notifications);

  AddChildViews(std::move(pinned_notification_list_view));
}

void NotificationCenterView::AddChildViews(
    std::unique_ptr<views::View> pinned_notification_list_view) {
  // TODO(crbug.com/1247455): Be able to do
  // SetContentsLayerType(LAYER_NOT_DRAWN).
  auto scroller_contents_view =
      views::Builder<views::BoxLayoutView>()
          .SetOrientation(views::BoxLayout::Orientation::kVertical)
          .SetBetweenChildSpacing(kMessageCenterPadding)
          .Build();
  if (features::AreOngoingProcessesEnabled()) {
    scroller_contents_view->AddChildView(
        std::move(pinned_notification_list_view));
  }
  scroller_contents_view->AddChildView(notification_list_view_);
  scroller_->SetContents(std::move(scroller_contents_view));
  // Need to set the transparent background explicitly, since ScrollView has
  // set the default opaque background color.
  scroller_->SetBackgroundColor(std::nullopt);
  scroller_->SetVerticalScrollBar(base::WrapUnique(scroll_bar_.get()));
  scroller_->SetDrawOverflowIndicator(false);
  scroller_->SetPaintToLayer();
  scroller_->layer()->SetRoundedCornerRadius(gfx::RoundedCornersF{
      static_cast<float>(kMessageCenterScrollViewCornerRadius)});

  AddChildView(scroller_.get());

  // Make sure the scroll view takes up the entirety of available height in the
  // notification center view. We're relying on a max height constraint
  // for the `TrayBubbleView` so we need to set flex for the scroll view here.
  scroller_->AddObserver(this);
  scroller_->ClipHeightTo(0, INT_MAX);
  layout_manager_->SetFlexForView(scroller_, 1);

  on_contents_scrolled_subscription_ =
      scroller_->AddContentsScrolledCallback(base::BindRepeating(
          &NotificationCenterView::OnContentsScrolled, base::Unretained(this)));

  AddChildView(notification_bar_.get());
}

bool NotificationCenterView::UpdateNotificationBar() {
  if (!notification_list_view_) {
    return false;
  }

  return notification_bar_->Update(
      notification_list_view_->GetTotalNotificationCount(),
      notification_list_view_->GetTotalPinnedNotificationCount(),
      GetStackedNotifications());
}

void NotificationCenterView::ClearAllNotifications() {
  if (!notification_list_view_) {
    return;
  }

  base::RecordAction(
      base::UserMetricsAction("StatusArea_Notifications_StackingBarClearAll"));

  notification_list_view_->ClearAllWithAnimation();
}

bool NotificationCenterView::IsScrollBarVisible() const {
  return scroll_bar_->GetVisible();
}

void NotificationCenterView::OnNotificationSlidOut() {
  UpdateNotificationBar();
}

void NotificationCenterView::OnNotificationAdded(const std::string& id) {
  CHECK(features::IsNotificationCenterControllerEnabled());
  if (!notification_list_view_) {
    return;
  }

  notification_list_view_->OnNotificationAdded(id);
}

void NotificationCenterView::OnNotificationRemoved(const std::string& id,
                                                   bool by_user) {
  CHECK(features::IsNotificationCenterControllerEnabled());
  if (!notification_list_view_) {
    return;
  }

  notification_list_view_->OnNotificationRemoved(id, by_user);
}

void NotificationCenterView::OnNotificationUpdated(const std::string& id) {
  CHECK(features::IsNotificationCenterControllerEnabled());
  if (!notification_list_view_) {
    return;
  }

  notification_list_view_->OnNotificationUpdated(id);
}

void NotificationCenterView::ListPreferredSizeChanged() {
  PreferredSizeChanged();

  if (GetWidget() && !GetWidget()->IsClosed()) {
    GetWidget()->SynthesizeMouseMoveEvent();
  }
}

void NotificationCenterView::ConfigureMessageView(
    message_center::MessageView* message_view) {
  message_view->set_scroller(scroller_);
}

void NotificationCenterView::OnViewBoundsChanged(views::View* observed_view) {
  UpdateNotificationBar();
}

void NotificationCenterView::OnContentsScrolled() {
  UpdateNotificationBar();
}

std::vector<raw_ptr<message_center::Notification, VectorExperimental>>
NotificationCenterView::GetStackedNotifications() const {
  if (!notification_list_view_) {
    return std::vector<
        raw_ptr<message_center::Notification, VectorExperimental>>{};
  }

  // CountNotificationsAboveY() only works after SetBoundsRect() is called at
  // least once.
  if (scroller_->bounds().IsEmpty()) {
    scroller_->SetBoundsRect(GetContentsBounds());
  }

  const int y_offset = scroller_->GetVisibleRect().bottom() - scroller_->y();
  return notification_list_view_->GetNotificationsBelowY(y_offset);
}

std::vector<std::string>
NotificationCenterView::GetNonVisibleNotificationIdsInViewHierarchy() const {
  if (!notification_list_view_) {
    return std::vector<std::string>{};
  }

  // CountNotificationsAboveY() only works after SetBoundsRect() is called at
  // least once.
  if (scroller_->bounds().IsEmpty()) {
    scroller_->SetBoundsRect(GetContentsBounds());
  }

  const int y_offset_above = scroller_->GetVisibleRect().y() - scroller_->y() +
                             kStackedNotificationBarHeight;
  auto above_id_list =
      notification_list_view_->GetNotificationIdsAboveY(y_offset_above);
  const int y_offset_below =
      scroller_->GetVisibleRect().bottom() - scroller_->y();
  const auto below_id_list =
      notification_list_view_->GetNotificationIdsBelowY(y_offset_below);
  above_id_list.insert(above_id_list.end(), below_id_list.begin(),
                       below_id_list.end());
  return above_id_list;
}

BEGIN_METADATA(NotificationCenterView)
END_METADATA

}  // namespace ash