chromium/chrome/browser/ui/views/editor_menu/editor_menu_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 "chrome/browser/ui/views/editor_menu/editor_menu_view.h"

#include <algorithm>
#include <array>
#include <string_view>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ui/views/editor_menu/editor_menu_badge_view.h"
#include "chrome/browser/ui/views/editor_menu/editor_menu_chip_view.h"
#include "chrome/browser/ui/views/editor_menu/editor_menu_strings.h"
#include "chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.h"
#include "chrome/browser/ui/views/editor_menu/editor_menu_view_delegate.h"
#include "chrome/browser/ui/views/editor_menu/utils/pre_target_handler.h"
#include "chrome/browser/ui/views/editor_menu/utils/pre_target_handler_view.h"
#include "chrome/browser/ui/views/editor_menu/utils/utils.h"
#include "chromeos/components/editor_menu/public/cpp/icon.h"
#include "chromeos/components/editor_menu/public/cpp/preset_text_query.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_manager.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/style/typography.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

namespace chromeos::editor_menu {

namespace {

constexpr char kWidgetName[] = "EditorMenuViewWidget";
constexpr char16_t kCardShownAnnouncement[] =
    u"Help Me Write, press tab to focus the Help Me Write card.";

constexpr gfx::Insets kTitleContainerInsets = gfx::Insets::TLBR(12, 16, 12, 14);

constexpr int kBadgeHorizontalPadding = 8;

// Spacing to apply between and around chips.
constexpr int kChipsHorizontalPadding = 8;
constexpr int kChipsVerticalPadding = 12;

constexpr gfx::Insets kChipsContainerInsets = gfx::Insets::TLBR(0, 16, 16, 16);

constexpr gfx::Insets kTextfieldContainerInsets =
    gfx::Insets::TLBR(0, 16, 12, 16);

}  // namespace

int GetChipsContainerHeightWithPaddings(int chip_height, int num_rows) {
  const int total_chips_height = num_rows * chip_height;
  const int total_chips_paddings =
      num_rows >= 1 ? (num_rows - 1) * kChipsVerticalPadding +
                          kChipsContainerInsets.height()
                    : 0;
  return total_chips_height + total_chips_paddings;
}

EditorMenuView::EditorMenuView(EditorMenuMode editor_menu_mode,
                               const PresetTextQueries& preset_text_queries,
                               const gfx::Rect& anchor_view_bounds,
                               EditorMenuViewDelegate* delegate)
    : PreTargetHandlerView(CardType::kEditorMenu),
      editor_menu_mode_(editor_menu_mode),
      delegate_(delegate) {
  CHECK(delegate_);
  InitLayout(preset_text_queries);

  GetViewAccessibility().SetRole(ax::mojom::Role::kDialog);
  GetViewAccessibility().SetName(editor_menu_mode_ == EditorMenuMode::kWrite
                                     ? GetEditorMenuWriteCardTitle()
                                     : GetEditorMenuRewriteCardTitle());
}

EditorMenuView::~EditorMenuView() = default;

// static
std::unique_ptr<views::Widget> EditorMenuView::CreateWidget(
    EditorMenuMode editor_menu_mode,
    const PresetTextQueries& preset_text_queries,
    const gfx::Rect& anchor_view_bounds,
    EditorMenuViewDelegate* delegate) {
  views::Widget::InitParams params(
      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_POPUP);
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.activatable = views::Widget::InitParams::Activatable::kYes;
  params.shadow_elevation = 2;
  params.shadow_type = views::Widget::InitParams::ShadowType::kDrop;
  params.z_order = ui::ZOrderLevel::kFloatingUIElement;
  params.name = kWidgetName;

  auto widget = std::make_unique<views::Widget>(std::move(params));
  EditorMenuView* editor_menu_view =
      widget->SetContentsView(std::make_unique<EditorMenuView>(
          editor_menu_mode, preset_text_queries, anchor_view_bounds, delegate));
  editor_menu_view->UpdateBounds(anchor_view_bounds);

  return widget;
}

void EditorMenuView::AddedToWidget() {
  PreTargetHandlerView::AddedToWidget();
  AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
}

void EditorMenuView::RequestFocus() {
  views::View::RequestFocus();
  settings_button_->RequestFocus();
}

gfx::Size EditorMenuView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  if (!available_size.width().is_bounded()) {
    return PreTargetHandlerView::CalculatePreferredSize(available_size);
  }

  int width = available_size.width().value();
  // When the width of editor menu view is updated, we will adjust the number of
  // rows chips (see: UpdateChipsContainer). Thus, here we need to pre-compute
  // the expected number of rows here and so we can estimate the height rather
  // than relying on the default logic.

  const int chip_container_width = width - kChipsContainerInsets.width();
  int running_width = 0;
  int num_rows = 0;
  int chip_height = 0;

  for (views::View* row : chips_container_->children()) {
    for (views::View* chip : row->children()) {
      const int chip_width = chip->GetPreferredSize().width();
      if (num_rows > 0 &&
          running_width + kChipsHorizontalPadding + chip_width <=
              chip_container_width) {
        running_width += kChipsHorizontalPadding + chip_width;
      } else {
        ++num_rows;
        running_width = chip_width;
      }

      chip_height = chip->height();
    }
  }

  const int title_height_with_padding =
      title_container_->height() + kTitleContainerInsets.height();
  const int chips_height_with_padding =
      GetChipsContainerHeightWithPaddings(chip_height, num_rows);
  const int textfield_height_with_padding =
      textfield_->height() + kTextfieldContainerInsets.height();

  return gfx::Size(width, title_height_with_padding +
                              chips_height_with_padding +
                              textfield_height_with_padding);
}

bool EditorMenuView::AcceleratorPressed(const ui::Accelerator& accelerator) {
  CHECK_EQ(accelerator.key_code(), ui::VKEY_ESCAPE);
  GetWidget()->Close();
  return true;
}

void EditorMenuView::OnWidgetVisibilityChanged(views::Widget* widget,
                                               bool visible) {
  CHECK(delegate_);
  delegate_->OnEditorMenuVisibilityChanged(visible);

  if (visible && !queued_announcement_) {
    GetViewAccessibility().AnnounceAlert(kCardShownAnnouncement);
    queued_announcement_ = true;
  }
}

void EditorMenuView::UpdateBounds(const gfx::Rect& anchor_view_bounds) {
  gfx::Rect editor_menu_bounds = GetEditorMenuBounds(anchor_view_bounds, this);
  GetWidget()->SetBounds(editor_menu_bounds);
  UpdateChipsContainer(/*editor_menu_width=*/editor_menu_bounds.width());
}

void EditorMenuView::DisableMenu() {
  settings_button_->SetEnabled(false);

  for (views::View* row : chips_container_->children()) {
    for (views::View* chip : row->children()) {
      chip->SetEnabled(false);
    }
  }

  textfield_->textfield()->SetEnabled(false);
  textfield_->arrow_button()->SetEnabled(false);
}

void EditorMenuView::InitLayout(const PresetTextQueries& preset_text_queries) {
  SetBackground(views::CreateThemedRoundedRectBackground(
      ui::kColorPrimaryBackground,
      views::LayoutProvider::Get()->GetCornerRadiusMetric(
          views::ShapeContextTokens::kMenuRadius)));

  auto* layout = SetLayoutManager(std::make_unique<views::FlexLayout>());
  layout->SetOrientation(views::LayoutOrientation::kVertical);

  AddTitleContainer();
  AddChipsContainer(preset_text_queries);
  AddTextfield();
}

void EditorMenuView::AddTitleContainer() {
  title_container_ = AddChildView(std::make_unique<views::View>());
  views::BoxLayout* layout =
      title_container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
          views::BoxLayout::Orientation::kHorizontal));
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  auto* title = title_container_->AddChildView(std::make_unique<views::Label>(
      editor_menu_mode_ == EditorMenuMode::kWrite
          ? GetEditorMenuWriteCardTitle()
          : GetEditorMenuRewriteCardTitle(),
      views::style::CONTEXT_DIALOG_TITLE, views::style::STYLE_HEADLINE_5));
  title->SetEnabledColorId(ui::kColorSysOnSurface);

  auto* badge =
      title_container_->AddChildView(std::make_unique<EditorMenuBadgeView>());
  badge->SetProperty(views::kMarginsKey,
                     gfx::Insets::VH(0, kBadgeHorizontalPadding));

  auto* spacer =
      title_container_->AddChildView(std::make_unique<views::View>());
  layout->SetFlexForView(spacer, 1);

  settings_button_ =
      title_container_->AddChildView(views::ImageButton::CreateIconButton(
          base::BindRepeating(&EditorMenuView::OnSettingsButtonPressed,
                              weak_factory_.GetWeakPtr()),
          vector_icons::kSettingsOutlineIcon, GetEditorMenuSettingsTooltip()));

  title_container_->SetProperty(views::kMarginsKey, kTitleContainerInsets);
}

void EditorMenuView::AddChipsContainer(
    const PresetTextQueries& preset_text_queries) {
  chips_container_ = AddChildView(std::make_unique<views::FlexLayoutView>());
  chips_container_->SetOrientation(views::LayoutOrientation::kVertical);
  chips_container_->SetCollapseMargins(true);
  chips_container_->SetIgnoreDefaultMainAxisMargins(true);
  chips_container_->SetProperty(views::kMarginsKey, kChipsContainerInsets);
  chips_container_->SetDefault(views::kMarginsKey,
                               gfx::Insets::VH(kChipsVerticalPadding, 0));

  // Put all the chips in a single row while we are initially creating the
  // editor menu. This layout will be adjusted once the editor menu bounds are
  // set.
  auto* row = AddChipsRow();
  for (const auto& preset_text_query : preset_text_queries) {
    row->AddChildView(std::make_unique<EditorMenuChipView>(
        base::BindRepeating(&EditorMenuView::OnChipButtonPressed,
                            weak_factory_.GetWeakPtr(),
                            preset_text_query.text_query_id),
        preset_text_query));
  }
}

void EditorMenuView::AddTextfield() {
  textfield_ = AddChildView(
      std::make_unique<EditorMenuTextfieldView>(editor_menu_mode_, delegate_));
  textfield_->SetProperty(views::kMarginsKey, kTextfieldContainerInsets);
}

void EditorMenuView::UpdateChipsContainer(int editor_menu_width) {
  // Remove chips from their current rows and transfer ownership since we want
  // to add the chips to new rows.
  std::vector<std::unique_ptr<EditorMenuChipView>> chips;
  for (views::View* row : chips_container_->children()) {
    while (!row->children().empty()) {
      chips.push_back(row->RemoveChildViewT(
          views::AsViewClass<EditorMenuChipView>(row->children()[0])));
    }
  }

  // Remove existing rows from the chips container and delete them.
  chips_container_->RemoveAllChildViews();

  // Re-add the chips into new rows in the chips container, adjusting the layout
  // according to the updated editor menu width. We keep track of the running
  // width of the current chip row and start a new row of chips whenever the
  // chip being added can't fit into the current row.
  const int chip_container_width =
      editor_menu_width - kChipsContainerInsets.width();
  int running_width = 0;
  views::View* row = nullptr;
  for (auto& chip : chips) {
    const int chip_width = chip->GetPreferredSize().width();
    if (row != nullptr &&
        running_width + chip_width + kChipsHorizontalPadding <=
            chip_container_width) {
      // Add the chip to the current row if it can fit (including space for
      // padding between chips).
      running_width += chip_width + kChipsHorizontalPadding;
    } else {
      // Otherwise, create a new row for the chip.
      row = AddChipsRow();
      running_width = chip_width;
    }
    row->AddChildView(std::move(chip));
  }
}

views::View* EditorMenuView::AddChipsRow() {
  auto* row =
      chips_container_->AddChildView(std::make_unique<views::FlexLayoutView>());
  row->SetCollapseMargins(true);
  row->SetIgnoreDefaultMainAxisMargins(true);
  row->SetDefault(views::kMarginsKey,
                  gfx::Insets::VH(0, kChipsHorizontalPadding));
  return row;
}

void EditorMenuView::OnSettingsButtonPressed() {
  CHECK(delegate_);
  delegate_->OnSettingsButtonPressed();
}

void EditorMenuView::OnChipButtonPressed(const std::string& text_query_id) {
  CHECK(delegate_);
  delegate_->OnChipButtonPressed(text_query_id);
}

BEGIN_METADATA(EditorMenuView)
END_METADATA

}  // namespace chromeos::editor_menu