blob: 2d4f9660d895280be249dd7ed19b865fd24e7df6 [file] [log] [blame]
// Copyright 2025 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/webauthn/combined_selector_views.h"
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/strings/string_util.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/webauthn/sheet_models.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/radio_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/layout/table_layout_view.h"
#include "ui/views/style/typography.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
namespace {
// This value is used to group all `CombinedSelectorRadioButton` which are under
// the same `CombinedSelectorRowView`. The grouping is used for traversal and
// selection. The value is selected voluntarily in order not to conflict
// grouping from any parent views.
constexpr static int kGroupId = 1327;
} // namespace
CombinedSelectorRadioButton::CombinedSelectorRadioButton(Delegate* delegate,
int index)
: views::RadioButton(u"", kGroupId), delegate_(delegate), index_(index) {}
views::View* CombinedSelectorRadioButton::GetSelectedViewForGroup(int group) {
Views views;
GetRadioButtonsInList(group, &views);
const auto i = std::ranges::find_if(views, [](const views::View* view) {
return static_cast<const CombinedSelectorRadioButton*>(view)->GetChecked();
});
return (i == views.cend()) ? nullptr : *i;
}
void CombinedSelectorRadioButton::SetChecked(bool checked) {
if (checked == RadioButton::GetChecked()) {
return;
}
if (checked) {
Views other;
GetRadioButtonsInList(GetGroup(), &other);
for (views::View* peer : other) {
if (peer != this && IsViewClass<CombinedSelectorRadioButton>(peer)) {
static_cast<CombinedSelectorRadioButton*>(peer)->SetChecked(false);
}
}
delegate_->OnRadioButtonChecked(index_);
RequestFocus();
}
Checkbox::SetChecked(checked);
}
void CombinedSelectorRadioButton::GetRadioButtonsInList(int group,
Views* views) {
auto* row_view = parent();
if (!row_view) {
return;
}
auto* list_view = row_view->parent();
if (!list_view) {
return;
}
list_view->GetViewsInGroup(group, views);
}
bool CombinedSelectorRadioButton::SkipDefaultKeyEventProcessing(
const ui::KeyEvent& event) {
// The radio button would show the ink drop on return key press. Since the
// radio buttons in the combined selector are tab focusable
// (IsGroupFocusTraversable), this is not required. The return key should not
// be handled by the radio button.
return event.key_code() == ui::VKEY_RETURN
? false
: RadioButton::SkipDefaultKeyEventProcessing(event);
}
BEGIN_METADATA(CombinedSelectorRadioButton)
END_METADATA
CombinedSelectorTextColumnView::CombinedSelectorTextColumnView(
const std::vector<std::u16string_view> texts) {
AddColumn(views::LayoutAlignment::kStart, views::LayoutAlignment::kCenter,
1.0f, views::TableLayout::ColumnSize::kFixed, 0, 0);
AddRows(texts.size(), views::TableLayout::kFixedSize);
for (size_t i = 0; i < texts.size(); i++) {
auto* label_view = AddChildView(std::make_unique<views::Label>(
texts.at(i), views::style::CONTEXT_LABEL,
i == 0 ? views::style::STYLE_BODY_3_MEDIUM
: views::style::STYLE_BODY_4));
label_view->SetHorizontalAlignment(gfx::ALIGN_LEFT);
}
}
BEGIN_METADATA(CombinedSelectorTextColumnView)
END_METADATA
CombinedSelectorRowView::CombinedSelectorRowView(
const ui::ImageModel& icon,
const std::vector<std::u16string_view> texts,
RadioStatus radio_status,
bool enabled,
CombinedSelectorRadioButton::Delegate* radio_delegate,
int index)
: radio_status_(radio_status), enabled_(enabled) {
SetEnabled(enabled);
GetViewAccessibility().SetRole(radio_status != RadioStatus::kNone
? ax::mojom::Role::kRadioButton
: ax::mojom::Role::kButton);
GetViewAccessibility().SetName(base::JoinString(texts, u"\n"));
const int horizontal_padding = radio_status == RadioStatus::kNone ? 0 : 16;
SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(8, horizontal_padding)));
const int icon_padding = ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_RELATED_LABEL_HORIZONTAL);
AddColumn(views::LayoutAlignment::kCenter, views::LayoutAlignment::kCenter,
views::TableLayout::kFixedSize,
views::TableLayout::ColumnSize::kUsePreferred,
/*fixed_width=*/0,
/*min_width=*/0)
.AddPaddingColumn(views::TableLayout::kFixedSize, icon_padding);
AddColumn(views::LayoutAlignment::kStretch, views::LayoutAlignment::kStretch,
/*horizontal_resize=*/1.0,
views::TableLayout::ColumnSize::kUsePreferred,
/*fixed_width=*/0,
/*min_width=*/0);
if (radio_status != RadioStatus::kNone) {
AddPaddingColumn(views::TableLayout::kFixedSize, icon_padding)
.AddColumn(views::LayoutAlignment::kCenter,
views::LayoutAlignment::kCenter,
views::TableLayout::kFixedSize,
views::TableLayout::ColumnSize::kUsePreferred,
/*fixed_width=*/0,
/*min_width=*/0);
}
AddRows(1, views::TableLayout::kFixedSize);
AddChildView(std::make_unique<views::ImageView>(icon));
AddChildView(std::make_unique<CombinedSelectorTextColumnView>(texts));
MaybeAddRadioButton(radio_delegate, index);
}
void CombinedSelectorRowView::MaybeAddRadioButton(
CombinedSelectorRadioButton::Delegate* delegate,
int index) {
if (radio_status_ == RadioStatus::kNone) {
return;
}
auto radio_button =
std::make_unique<CombinedSelectorRadioButton>(delegate, index);
radio_button->SetChecked(radio_status_ == RadioStatus::kSelected);
radio_button->SetEnabled(enabled_);
radio_button->GetViewAccessibility().SetName(*this);
radio_button_ = AddChildView(std::move(radio_button));
}
void CombinedSelectorRowView::RequestFocus() {
if (radio_button_) {
radio_button_->RequestFocus();
}
}
bool CombinedSelectorRowView::OnMousePressed(const ui::MouseEvent& event) {
if (radio_button_ && event.IsOnlyLeftMouseButton()) {
const gfx::Point center = radio_button_->GetLocalBounds().CenterPoint();
ui::MouseEvent synthetic_press_event(
ui::EventType::kMousePressed, center, center, event.time_stamp(),
event.flags(), event.changed_button_flags());
radio_button_->OnMousePressed(synthetic_press_event);
return true;
}
return views::TableLayoutView::OnMousePressed(event);
}
void CombinedSelectorRowView::OnMouseReleased(const ui::MouseEvent& event) {
if (radio_button_ && event.IsOnlyLeftMouseButton()) {
const gfx::Point center = radio_button_->GetLocalBounds().CenterPoint();
ui::MouseEvent synthetic_release_event(
ui::EventType::kMouseReleased, center, center, event.time_stamp(),
event.flags(), event.changed_button_flags());
radio_button_->OnMouseReleased(synthetic_release_event);
RequestFocus();
return;
}
views::TableLayoutView::OnMouseReleased(event); // Default handling.
}
BEGIN_METADATA(CombinedSelectorRowView)
END_METADATA
CombinedSelectorListView::CombinedSelectorListView(
CombinedSelectorSheetModel* model,
CombinedSelectorRadioButton::Delegate* delegate) {
SetLayoutManager(std::make_unique<views::FillLayout>());
auto* scroll_view = AddChildView(std::make_unique<views::ScrollView>());
auto wrapper = std::make_unique<views::View>();
wrapper->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
/*between_child_spacing=*/kRowGap));
size_t num_mechanisms = model->dialog_model()->mechanisms.size();
if (num_mechanisms > 1) {
wrapper->AddChildView(std::make_unique<views::Separator>());
}
for (size_t i = 0; i < num_mechanisms; i++) {
if (i > 0) {
wrapper->AddChildView(std::make_unique<views::Separator>());
}
const auto& mechanism = model->dialog_model()->mechanisms[i];
auto image_model =
ui::ImageModel::FromVectorIcon(*mechanism.icon, ui::kColorIcon, 20);
auto texts = mechanism.display_name.empty() ||
(mechanism.display_name == mechanism.name)
? std::vector<std::u16string_view>{mechanism.name,
mechanism.description}
: std::vector<std::u16string_view>{mechanism.display_name,
mechanism.name,
mechanism.description};
auto* row = wrapper->AddChildView(std::make_unique<CombinedSelectorRowView>(
image_model, std::move(texts), model->GetSelectionStatus(i),
!model->dialog_model()->ui_disabled_, delegate, i));
if (model->GetSelectionStatus(i) ==
CombinedSelectorSheetModel::SelectionStatus::kSelected) {
selected_view_ = row;
}
SetOwnedGroup(kGroupId);
}
if (num_mechanisms > 1) {
wrapper->AddChildView(std::make_unique<views::Separator>());
}
scroll_view->SetContents(std::move(wrapper));
scroll_view->ClipHeightTo(kMaxRowHeight, 3 * kMaxRowHeight + 2 * kRowGap);
}
CombinedSelectorListView::~CombinedSelectorListView() = default;
void CombinedSelectorListView::RequestFocus() {
if (selected_view_) {
selected_view_->RequestFocus();
}
}
BEGIN_METADATA(CombinedSelectorListView)
END_METADATA