blob: a517d74291109b9debd111a761df8cab766c9802 [file] [log] [blame]
// Copyright 2017 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/payments/editor_view_controller.h"
#include <algorithm>
#include <map>
#include <memory>
#include <utility>
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/payments/payment_request_dialog_view.h"
#include "chrome/browser/ui/views/payments/payment_request_dialog_view_ids.h"
#include "chrome/browser/ui/views/payments/payment_request_sheet_controller.h"
#include "chrome/browser/ui/views/payments/payment_request_views_util.h"
#include "chrome/browser/ui/views/payments/validating_combobox.h"
#include "chrome/browser/ui/views/payments/validating_textfield.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/combobox_model.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/table_layout_view.h"
#include "ui/views/view.h"
namespace payments {
namespace {
constexpr int kErrorLabelTopPadding = 6;
std::unique_ptr<views::Label> CreateErrorLabel(const std::u16string& error,
autofill::FieldType type) {
return views::Builder<views::Label>()
.SetText(error)
.SetTextContext(CONTEXT_DIALOG_BODY_TEXT_SMALL)
.SetID(static_cast<int>(DialogViewID::ERROR_LABEL_OFFSET) + type)
.SetMultiLine(true)
.SetHorizontalAlignment(gfx::ALIGN_LEFT)
.SetBorder(views::CreateEmptyBorder(
gfx::Insets::TLBR(kErrorLabelTopPadding, 0, 0, 0)))
.SetEnabledColor(ui::kColorAlertHighSeverity)
.Build();
}
} // namespace
EditorViewController::EditorViewController(
base::WeakPtr<PaymentRequestSpec> spec,
base::WeakPtr<PaymentRequestState> state,
base::WeakPtr<PaymentRequestDialogView> dialog,
BackNavigationType back_navigation_type,
bool is_incognito)
: PaymentRequestSheetController(spec, state, dialog),
back_navigation_type_(back_navigation_type),
is_incognito_(is_incognito) {}
EditorViewController::~EditorViewController() {
ClearViewPointers();
}
void EditorViewController::DisplayErrorMessageForField(
autofill::FieldType type,
const std::u16string& error_message) {
AddOrUpdateErrorMessageForField(type, error_message);
RelayoutPane();
}
// static
int EditorViewController::GetInputFieldViewId(autofill::FieldType type) {
return static_cast<int>(DialogViewID::INPUT_FIELD_TYPE_OFFSET) +
static_cast<int>(type);
}
std::unique_ptr<views::View> EditorViewController::CreateHeaderView() {
return nullptr;
}
std::unique_ptr<views::View> EditorViewController::CreateCustomFieldView(
autofill::FieldType type,
views::View** focusable_field,
bool* valid,
std::u16string* error_message) {
return nullptr;
}
std::unique_ptr<views::View> EditorViewController::CreateExtraViewForField(
autofill::FieldType type) {
return nullptr;
}
bool EditorViewController::ValidateInputFields() {
for (const auto& field : text_fields()) {
if (!field.first->IsValid()) {
return false;
}
}
for (const auto& field : comboboxes()) {
if (!field.first->IsValid()) {
return false;
}
}
return true;
}
void EditorViewController::Stop() {
PaymentRequestSheetController::Stop();
ClearViewPointers();
}
std::u16string EditorViewController::GetPrimaryButtonLabel() {
return l10n_util::GetStringUTF16(IDS_DONE);
}
PaymentRequestSheetController::ButtonCallback
EditorViewController::GetPrimaryButtonCallback() {
return base::BindRepeating(&EditorViewController::SaveButtonPressed,
base::Unretained(this));
}
int EditorViewController::GetPrimaryButtonId() {
return static_cast<int>(DialogViewID::EDITOR_SAVE_BUTTON);
}
bool EditorViewController::GetPrimaryButtonEnabled() {
return true;
}
bool EditorViewController::ShouldShowSecondaryButton() {
// Do not show the "Cancel Payment" button.
return false;
}
void EditorViewController::FillContentView(views::View* content_view) {
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical);
layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kStart);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStretch);
content_view->SetLayoutManager(std::move(layout));
// No insets. Child views below are responsible for their padding.
// An editor can optionally have a header view specific to it.
if (std::unique_ptr<views::View> header_view = CreateHeaderView()) {
content_view->AddChildView(std::move(header_view));
}
// The heart of the editor dialog: all the input fields with their labels.
content_view->AddChildView(CreateEditorView());
}
bool EditorViewController::ShouldAccelerateEnterKey() {
// We allow the user to confirm their details by pressing 'enter' irregardless
// of which edit field is currently focused, for quicker navigation through
// the form.
return true;
}
void EditorViewController::UpdateEditorView() {
// `UpdateContentView` removes all children, so reset pointers to them.
ClearViewPointers();
UpdateContentView();
UpdateFocus(GetFirstFocusedView());
dialog()->EditorViewUpdated();
}
views::View* EditorViewController::GetFirstFocusedView() {
if (initial_focus_field_view_) {
return initial_focus_field_view_;
}
return PaymentRequestSheetController::GetFirstFocusedView();
}
std::unique_ptr<ValidatingCombobox>
EditorViewController::CreateComboboxForField(const EditorField& field,
std::u16string* error_message) {
std::unique_ptr<ValidationDelegate> delegate =
CreateValidationDelegate(field);
ValidationDelegate* delegate_ptr = delegate.get();
std::unique_ptr<ValidatingCombobox> combobox =
std::make_unique<ValidatingCombobox>(GetComboboxModelForType(field.type),
std::move(delegate));
combobox->GetViewAccessibility().SetName(field.label);
std::u16string initial_value = GetInitialValueForType(field.type);
if (!initial_value.empty()) {
combobox->SelectValue(initial_value);
}
if (IsEditingExistingItem()) {
combobox->SetInvalid(
!delegate_ptr->IsValidCombobox(combobox.get(), error_message));
}
// Using autofill field type as a view ID.
combobox->SetID(GetInputFieldViewId(field.type));
combobox->SetCallback(
base::BindRepeating(&EditorViewController::OnPerformAction,
base::Unretained(this), combobox.get()));
comboboxes_.insert(std::make_pair(combobox.get(), field));
return combobox;
}
void EditorViewController::ContentsChanged(views::Textfield* sender,
const std::u16string& new_contents) {
ValidatingTextfield* sender_cast = static_cast<ValidatingTextfield*>(sender);
sender_cast->OnContentsChanged();
primary_button()->SetEnabled(ValidateInputFields());
}
void EditorViewController::OnPerformAction(ValidatingCombobox* sender) {
static_cast<ValidatingCombobox*>(sender)->OnContentsChanged();
primary_button()->SetEnabled(ValidateInputFields());
}
std::unique_ptr<views::View> EditorViewController::CreateEditorView() {
std::unique_ptr<views::View> editor_view = std::make_unique<views::View>();
// All views have fixed size except the Field which stretches. The fixed
// padding at the end is computed so that Field views have a minimum of
// 176/272dp (short/long fields) as per spec.
// ___________________________________________________________________________
// |Label | 16dp pad | Field (flex) | 8dp pad | Extra View | Computed Padding|
// |______|__________|______________|_________|____________|_________________|
constexpr int kInputRowSpacing = 12;
editor_view->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
kInputRowSpacing, payments::kPaymentRequestRowHorizontalInsets, 0,
payments::kPaymentRequestRowHorizontalInsets)));
editor_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
kInputRowSpacing));
views::View* first_field = nullptr;
for (const auto& field : GetFieldDefinitions()) {
bool valid = false;
views::View* focusable_field =
CreateInputField(editor_view.get(), field, &valid);
if (!first_field) {
first_field = focusable_field;
}
if (!initial_focus_field_view_ && !valid) {
initial_focus_field_view_ = focusable_field;
}
}
if (!initial_focus_field_view_) {
initial_focus_field_view_ = first_field;
}
// Validate all fields and disable the primary (Done) button if necessary.
primary_button()->SetEnabled(ValidateInputFields());
// Adds the "* indicates a required field" label in "hint" grey text.
editor_view->AddChildView(CreateHintLabel(
l10n_util::GetStringUTF16(IDS_PAYMENTS_REQUIRED_FIELD_MESSAGE),
gfx::HorizontalAlignment::ALIGN_LEFT));
return editor_view;
}
// Each input field is a 4-quadrant grid.
// +----------------------------------------------------------+
// | Field Label | Input field (textfield/combobox) |
// |_______________________|__________________________________|
// | (empty) | Error label |
// +----------------------------------------------------------+
views::View* EditorViewController::CreateInputField(views::View* editor_view,
const EditorField& field,
bool* valid) {
constexpr int kShortFieldMinimumWidth = 176;
constexpr int kLabelWidth = 140;
constexpr int kLongFieldMinimumWidth = 272;
// This is the horizontal padding between the label and the field.
const int label_input_field_horizontal_padding =
ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
// The horizontal padding between the field and the extra view.
constexpr int kFieldExtraViewHorizontalPadding = 8;
auto* field_view =
editor_view->AddChildView(std::make_unique<views::TableLayoutView>());
// Label.
field_view->AddColumn(views::LayoutAlignment::kStart,
views::LayoutAlignment::kCenter,
views::TableLayout::kFixedSize,
views::TableLayout::ColumnSize::kFixed, kLabelWidth, 0);
field_view->AddPaddingColumn(views::TableLayout::kFixedSize,
label_input_field_horizontal_padding);
// Field.
field_view->AddColumn(views::LayoutAlignment::kStretch,
views::LayoutAlignment::kStretch, 1.0,
views::TableLayout::ColumnSize::kUsePreferred, 0, 0);
field_view->AddPaddingColumn(views::TableLayout::kFixedSize,
kFieldExtraViewHorizontalPadding);
int extra_view_width, padding, field_width;
if (field.length_hint == EditorField::LengthHint::HINT_SHORT) {
field_width = kShortFieldMinimumWidth;
extra_view_width =
ComputeWidestExtraViewWidth(EditorField::LengthHint::HINT_SHORT);
// The padding at the end is fixed, computed to make sure the short field
// maintains its minimum width.
} else {
field_width = kLongFieldMinimumWidth;
extra_view_width =
ComputeWidestExtraViewWidth(EditorField::LengthHint::HINT_LONG);
}
// The padding at the end is fixed, computed to make sure the long field
// maintains its minimum width.
padding = kDialogMinWidth - field_width - kLabelWidth -
(2 * kPaymentRequestRowHorizontalInsets) -
label_input_field_horizontal_padding -
kFieldExtraViewHorizontalPadding - extra_view_width;
// Extra view.
field_view->AddColumn(
views::LayoutAlignment::kStart, views::LayoutAlignment::kCenter,
views::TableLayout::kFixedSize, views::TableLayout::ColumnSize::kFixed,
extra_view_width, 0);
field_view->AddPaddingColumn(views::TableLayout::kFixedSize, padding);
field_view->AddRows(1, views::TableLayout::kFixedSize);
std::unique_ptr<views::Label> label = std::make_unique<views::Label>(
field.required ? field.label + u"*" : field.label);
label->SetMultiLine(true);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
field_view->AddChildView(std::move(label));
views::View* focusable_field = nullptr;
constexpr int kInputFieldHeight = 28;
std::u16string error_message;
switch (field.control_type) {
case EditorField::ControlType::TEXTFIELD:
case EditorField::ControlType::TEXTFIELD_NUMBER: {
std::unique_ptr<ValidationDelegate> validation_delegate =
CreateValidationDelegate(field);
ValidationDelegate* delegate_ptr = validation_delegate.get();
std::u16string initial_value = GetInitialValueForType(field.type);
auto text_field =
std::make_unique<ValidatingTextfield>(std::move(validation_delegate));
// Set the initial value and validity state.
text_field->SetText(initial_value);
text_field->GetViewAccessibility().SetName(field.label);
*valid = IsEditingExistingItem() &&
delegate_ptr->IsValidTextfield(text_field.get(), &error_message);
if (IsEditingExistingItem()) {
text_field->SetInvalid(!(*valid));
}
if (field.control_type == EditorField::ControlType::TEXTFIELD_NUMBER) {
text_field->SetTextInputType(ui::TextInputType::TEXT_INPUT_TYPE_NUMBER);
}
text_field->set_controller(this);
// Using autofill field type as a view ID (for testing).
text_field->SetID(GetInputFieldViewId(field.type));
text_fields_.insert(std::make_pair(text_field.get(), field));
focusable_field = field_view->AddChildView(std::move(text_field));
focusable_field->SetPreferredSize(
gfx::Size(field_width, kInputFieldHeight));
break;
}
case EditorField::ControlType::COMBOBOX: {
std::unique_ptr<ValidatingCombobox> combobox =
CreateComboboxForField(field, &error_message);
*valid = combobox->IsValid();
focusable_field = field_view->AddChildView(std::move(combobox));
focusable_field->SetPreferredSize(
gfx::Size(field_width, kInputFieldHeight));
break;
}
case EditorField::ControlType::CUSTOMFIELD: {
std::unique_ptr<views::View> custom_view = CreateCustomFieldView(
field.type, &focusable_field, valid, &error_message);
DCHECK(custom_view);
field_view->AddChildView(std::move(custom_view));
field_view->SetPreferredSize(gfx::Size(field_width, kInputFieldHeight));
break;
}
case EditorField::ControlType::READONLY_LABEL: {
std::unique_ptr<views::Label> readonly_label =
std::make_unique<views::Label>(GetInitialValueForType(field.type));
readonly_label->SetID(GetInputFieldViewId(field.type));
readonly_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
field_view->AddChildView(std::move(readonly_label));
field_view->SetPreferredSize(gfx::Size(field_width, kInputFieldHeight));
break;
}
}
// If an extra view needs to go alongside the input field view, add it to the
// last column.
std::unique_ptr<views::View> extra_view = CreateExtraViewForField(field.type);
field_view->AddChildView(extra_view ? std::move(extra_view)
: std::make_unique<views::View>());
// Error view.
field_view->AddRows(1, views::TableLayout::kFixedSize);
// Skip the first label column.
field_view->AddChildView(std::make_unique<views::View>());
auto error_label_view = std::make_unique<views::View>();
error_label_view->SetLayoutManager(std::make_unique<views::FillLayout>());
error_labels_[field.type] = error_label_view.get();
if (IsEditingExistingItem() && !error_message.empty()) {
AddOrUpdateErrorMessageForField(field.type, error_message);
}
field_view->AddChildView(std::move(error_label_view));
return focusable_field;
}
int EditorViewController::ComputeWidestExtraViewWidth(
EditorField::LengthHint size) {
int widest_column_width = 0;
for (const auto& field : GetFieldDefinitions()) {
if (field.length_hint != size) {
continue;
}
std::unique_ptr<views::View> extra_view =
CreateExtraViewForField(field.type);
if (!extra_view) {
continue;
}
widest_column_width =
std::max(extra_view->GetPreferredSize().width(), widest_column_width);
}
return widest_column_width;
}
void EditorViewController::AddOrUpdateErrorMessageForField(
autofill::FieldType type,
const std::u16string& error_message) {
const auto& label_view_it = error_labels_.find(type);
CHECK(label_view_it != error_labels_.end());
if (error_message.empty()) {
label_view_it->second->RemoveAllChildViews();
} else {
if (label_view_it->second->children().empty()) {
// If there was no error label view, add it.
label_view_it->second->AddChildView(
CreateErrorLabel(error_message, type));
} else {
// The error view is the only child, and has a Label as only child itself.
static_cast<views::Label*>(label_view_it->second->children().front())
->SetText(error_message);
}
}
}
void EditorViewController::SaveButtonPressed(const ui::Event& event) {
if (!ValidateModelAndSave()) {
return;
}
if (back_navigation_type_ == BackNavigationType::kOneStep) {
dialog()->GoBack();
} else {
DCHECK_EQ(BackNavigationType::kPaymentSheet, back_navigation_type_);
dialog()->GoBackToPaymentSheet();
}
}
void EditorViewController::ClearViewPointers() {
for (const auto& [textfield, _] : text_fields_) {
textfield->set_controller(nullptr);
}
text_fields_.clear();
comboboxes_.clear();
initial_focus_field_view_ = nullptr;
}
} // namespace payments