chromium/ash/system/media/quick_settings_media_view.cc

// Copyright 2023 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/media/quick_settings_media_view.h"

#include "ash/public/cpp/pagination/pagination_controller.h"
#include "ash/public/cpp/pagination/pagination_model_observer.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/pagination_view.h"
#include "ash/system/media/quick_settings_media_view_controller.h"
#include "components/global_media_controls/public/views/media_item_ui_view.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/compositor/layer.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout_view.h"

namespace ash {

namespace {

// The width of the media view.
constexpr int kMediaViewWidth =
    global_media_controls::kCrOSMediaItemUpdatedUISize.width();

// The height of the media view if there are multiple media items, which only
// needs to be defined here for QuickSettingsMediaView because it is different
// from the height when there is only one media item.
constexpr int kMultipleMediaViewHeight = 170;

// The y-position of the floating pagination dots view in the media view.
constexpr int kPaginationViewHeight = 140;

// A scroll view that arranges all the media items in a row and sets which item
// is shown on the screen by setting the x position of the scroll content.
class MediaScrollView : public views::ScrollView,
                        public PaginationModelObserver {
  METADATA_HEADER(MediaScrollView, views::ScrollView)

 public:
  MediaScrollView(QuickSettingsMediaView* media_view, PaginationModel* model)
      : views::ScrollView(ScrollView::ScrollWithLayers::kEnabled),
        media_view_(media_view),
        model_(model) {
    observer_.Observe(model_);
    SetContents(std::make_unique<views::BoxLayoutView>());

    // Remove the default background color.
    SetBackgroundColor(std::nullopt);

    // The scroll view does not accept any scroll event.
    SetHorizontalScrollBarMode(views::ScrollView::ScrollBarMode::kDisabled);
    SetVerticalScrollBarMode(views::ScrollView::ScrollBarMode::kDisabled);
  }

  MediaScrollView(const MediaScrollView&) = delete;
  MediaScrollView& operator=(const MediaScrollView&) = delete;
  ~MediaScrollView() override = default;

  // views::ScrollView:
  gfx::Size CalculatePreferredSize(
      const views::SizeBounds& available_size) const override {
    return gfx::Size(kMediaViewWidth, media_view_->GetMediaViewHeight());
  }

  void Layout(PassKey) override {
    contents()->SizeToPreferredSize();
    LayoutSuperclass<views::ScrollView>(this);
  }

  void ScrollRectToVisible(const gfx::Rect& rect) override {
    // A tab key event can focus a UI element on the previous/next page so we
    // need to scroll to that page automatically.
    const int rect_page = rect.x() / kMediaViewWidth;
    if (rect_page != model_->selected_page()) {
      model_->SelectPage(rect_page, /*animate=*/true);
    }
  }

  // PaginationModelObserver:
  void SelectedPageChanged(int old_selected, int new_selected) override {
    if (model_->is_valid_page(new_selected)) {
      ScrollToOffset(gfx::PointF(new_selected * kMediaViewWidth, 0));
    }
  }

  void TransitionChanged() override {
    // Update the scroll content to show the transition progress of changing
    // from one media item to another.
    const int origin = model_->selected_page() * kMediaViewWidth;
    const int target = model_->transition().target_page * kMediaViewWidth;
    const double progress = model_->transition().progress;
    ScrollToOffset(
        gfx::PointF(gfx::Tween::IntValueBetween(progress, origin, target), 0));
  }

 private:
  // |media_view_| is owned by the views hierarchy.
  raw_ptr<QuickSettingsMediaView> media_view_ = nullptr;
  raw_ptr<PaginationModel> model_ = nullptr;
  base::ScopedObservation<PaginationModel, PaginationModelObserver> observer_{
      this};
};

BEGIN_METADATA(MediaScrollView)
END_METADATA

}  // namespace

QuickSettingsMediaView::QuickSettingsMediaView(
    QuickSettingsMediaViewController* controller)
    : controller_(controller) {
  // All the views need to paint to layer so that the pagination view can be
  // placed floating above the media scroll view.
  media_scroll_view_ =
      AddChildView(std::make_unique<MediaScrollView>(this, &pagination_model_));

  pagination_view_ =
      AddChildView(std::make_unique<PaginationView>(&pagination_model_));
  pagination_view_->SetPaintToLayer();
  pagination_view_->layer()->SetFillsBoundsOpaquely(false);

  pagination_controller_ = std::make_unique<PaginationController>(
      &pagination_model_, PaginationController::SCROLL_AXIS_HORIZONTAL,
      base::BindRepeating([](ui::EventType) {}));
}

QuickSettingsMediaView::~QuickSettingsMediaView() = default;

///////////////////////////////////////////////////////////////////////////////
// views::View implementations:

gfx::Size QuickSettingsMediaView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return gfx::Size(kMediaViewWidth, GetMediaViewHeight());
}

void QuickSettingsMediaView::Layout(PassKey) {
  media_scroll_view_->SetBounds(0, 0, kMediaViewWidth, GetMediaViewHeight());

  // Place the pagination dots view on top of the media view.
  gfx::Size pagination_view_size = pagination_view_->CalculatePreferredSize({});
  pagination_view_->SetBounds(
      (kMediaViewWidth - pagination_view_size.width()) / 2,
      kPaginationViewHeight, pagination_view_size.width(),
      pagination_view_size.height());

  // The pagination dots view only shows if there are multiple media items.
  pagination_view_->SetVisible(items_.size() > 1);
}

void QuickSettingsMediaView::OnGestureEvent(ui::GestureEvent* event) {
  // The pagination controller will handle the touch gesture event to swipe
  // between media items.
  if (pagination_controller_->OnGestureEvent(*event, GetContentsBounds())) {
    event->SetHandled();
  } else if (event->type() == ui::EventType::kGestureTap) {
    // A tap gesture is handled in the same way as a mouse click event. The
    // controller does not need to know the item id for now so we do not need to
    // record it.
    controller_->OnMediaItemUIClicked(/*id=*/"",
                                      /*activate_original_media=*/false);
    event->SetHandled();
  }
}

///////////////////////////////////////////////////////////////////////////////
// QuickSettingsMediaView implementations:

void QuickSettingsMediaView::ShowItem(
    const std::string& id,
    std::unique_ptr<global_media_controls::MediaItemUIView> item) {
  DCHECK(!base::Contains(items_, id));
  items_[id] = media_scroll_view_->contents()->AddChildView(std::move(item));

  // Set the updated size of the media item based on the number of media items.
  items_[id]->SetPreferredSize(
      gfx::Size(kMediaViewWidth, GetMediaViewHeight()));

  pagination_model_.SetTotalPages(items_.size());
  PreferredSizeChanged();
  controller_->SetShowMediaView(true);
}

void QuickSettingsMediaView::HideItem(const std::string& id) {
  if (!base::Contains(items_, id)) {
    return;
  }
  media_scroll_view_->contents()->RemoveChildViewT(items_[id]);
  items_.erase(id);

  pagination_model_.SetTotalPages(items_.size());
  PreferredSizeChanged();
  controller_->SetShowMediaView(!items_.empty());
}

void QuickSettingsMediaView::UpdateItemOrder(std::list<std::string> ids) {
  if (ids.empty()) {
    return;
  }

  // Remove all the media views for re-ordering.
  std::map<const std::string,
           std::unique_ptr<global_media_controls::MediaItemUIView>>
      media_items;
  for (auto& item : items_) {
    media_items[item.first] =
        media_scroll_view_->contents()->RemoveChildViewT(item.second);
  }

  // Add back the media views given the new order.
  for (auto& id : ids) {
    DCHECK(base::Contains(media_items, id));
    items_[id] = media_scroll_view_->contents()->AddChildView(
        std::move(media_items[id]));
  }
}

int QuickSettingsMediaView::GetMediaViewHeight() const {
  return (items_.size() > 1)
             ? kMultipleMediaViewHeight
             : global_media_controls::kCrOSMediaItemUpdatedUISize.height();
}

BEGIN_METADATA(QuickSettingsMediaView)
END_METADATA

}  // namespace ash