blob: 4557672f053bc76e56790283112d5cdc195e1ae4 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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_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_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/models/combobox_model.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/native_theme/native_theme.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/fill_layout.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/view.h"
namespace payments {
namespace {
enum class EditorViewControllerTags : int {
// The tag for the button that saves the model being edited. Starts
// at PAYMENT_REQUEST_COMMON_TAG_MAX not to conflict with tags
// common to all views.
SAVE_BUTTON = static_cast<int>(
PaymentRequestCommonTags::PAYMENT_REQUEST_COMMON_TAG_MAX),
};
std::unique_ptr<views::View> CreateErrorLabelView(
const base::string16& error,
autofill::ServerFieldType type) {
std::unique_ptr<views::View> view = std::make_unique<views::View>();
std::unique_ptr<views::BoxLayout> layout =
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical);
layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_START);
layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_STRETCH);
// This is the space between the input field and the error label.
constexpr int kErrorLabelTopPadding = 6;
layout->set_inside_border_insets(gfx::Insets(kErrorLabelTopPadding, 0, 0, 0));
view->SetLayoutManager(std::move(layout));
std::unique_ptr<views::Label> error_label =
std::make_unique<views::Label>(error, CONTEXT_BODY_TEXT_SMALL);
error_label->set_id(static_cast<int>(DialogViewID::ERROR_LABEL_OFFSET) +
type);
error_label->SetEnabledColor(error_label->GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_AlertSeverityHigh));
error_label->SetMultiLine(true);
error_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
view->AddChildView(error_label.release());
return view;
}
} // namespace
EditorViewController::EditorViewController(
PaymentRequestSpec* spec,
PaymentRequestState* state,
PaymentRequestDialogView* dialog,
BackNavigationType back_navigation_type,
bool is_incognito)
: PaymentRequestSheetController(spec, state, dialog),
initial_focus_field_view_(nullptr),
back_navigation_type_(back_navigation_type),
is_incognito_(is_incognito) {}
EditorViewController::~EditorViewController() {}
void EditorViewController::DisplayErrorMessageForField(
autofill::ServerFieldType type,
const base::string16& error_message) {
AddOrUpdateErrorMessageForField(type, error_message);
RelayoutPane();
}
// static
int EditorViewController::GetInputFieldViewId(autofill::ServerFieldType 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::ServerFieldType type,
views::View** focusable_field,
bool* valid,
base::string16* error_message) {
return nullptr;
}
std::unique_ptr<views::View> EditorViewController::CreateExtraViewForField(
autofill::ServerFieldType 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;
}
std::unique_ptr<views::Button> EditorViewController::CreatePrimaryButton() {
std::unique_ptr<views::Button> button(
views::MdTextButton::CreateSecondaryUiBlueButton(
this, l10n_util::GetStringUTF16(IDS_DONE)));
button->set_tag(static_cast<int>(EditorViewControllerTags::SAVE_BUTTON));
button->set_id(static_cast<int>(DialogViewID::EDITOR_SAVE_BUTTON));
return button;
}
void EditorViewController::FillContentView(views::View* content_view) {
auto layout = std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical);
layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_START);
layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_STRETCH);
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.
std::unique_ptr<views::View> header_view = CreateHeaderView();
if (header_view.get())
content_view->AddChildView(header_view.release());
// The heart of the editor dialog: all the input fields with their labels.
content_view->AddChildView(CreateEditorView().release());
}
void EditorViewController::UpdateEditorView() {
UpdateContentView();
UpdateFocus(GetFirstFocusedView());
dialog()->EditorViewUpdated();
}
void EditorViewController::ButtonPressed(views::Button* sender,
const ui::Event& event) {
switch (sender->tag()) {
case static_cast<int>(EditorViewControllerTags::SAVE_BUTTON):
if (ValidateModelAndSave()) {
switch (back_navigation_type_) {
case BackNavigationType::kOneStep:
dialog()->GoBack();
break;
case BackNavigationType::kPaymentSheet:
dialog()->GoBackToPaymentSheet();
break;
}
}
break;
default:
PaymentRequestSheetController::ButtonPressed(sender, event);
break;
}
}
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,
base::string16* 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->SetAccessibleName(field.label);
base::string16 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->set_id(GetInputFieldViewId(field.type));
combobox->set_listener(this);
comboboxes_.insert(std::make_pair(combobox.get(), field));
return combobox;
}
void EditorViewController::ContentsChanged(views::Textfield* sender,
const base::string16& new_contents) {
ValidatingTextfield* sender_cast = static_cast<ValidatingTextfield*>(sender);
sender_cast->OnContentsChanged();
primary_button()->SetEnabled(ValidateInputFields());
}
void EditorViewController::OnPerformAction(views::Combobox* sender) {
ValidatingCombobox* sender_cast = static_cast<ValidatingCombobox*>(sender);
sender_cast->OnContentsChanged();
primary_button()->SetEnabled(ValidateInputFields());
}
std::unique_ptr<views::View> EditorViewController::CreateEditorView() {
std::unique_ptr<views::View> editor_view = std::make_unique<views::View>();
text_fields_.clear();
comboboxes_.clear();
initial_focus_field_view_ = nullptr;
// The editor view is padded horizontally.
editor_view->SetBorder(views::CreateEmptyBorder(
0, payments::kPaymentRequestRowHorizontalInsets, 0,
payments::kPaymentRequestRowHorizontalInsets));
// 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 kLabelWidth = 140;
// This is the horizontal padding between the label and the field.
constexpr int kLabelInputFieldHorizontalPadding = 16;
// This is the horizontal padding between the field and the extra view.
constexpr int kFieldExtraViewHorizontalPadding = 8;
constexpr int kShortFieldMinimumWidth = 176;
constexpr int kLongFieldMinimumWidth = 272;
views::GridLayout* editor_layout = editor_view->SetLayoutManager(
std::make_unique<views::GridLayout>(editor_view.get()));
// Column set for short fields.
views::ColumnSet* columns_short = editor_layout->AddColumnSet(0);
columns_short->AddColumn(
views::GridLayout::LEADING, views::GridLayout::CENTER,
views::GridLayout::kFixedSize, views::GridLayout::FIXED, kLabelWidth, 0);
columns_short->AddPaddingColumn(views::GridLayout::kFixedSize,
kLabelInputFieldHorizontalPadding);
// The field view column stretches.
columns_short->AddColumn(views::GridLayout::LEADING,
views::GridLayout::CENTER, 1.0,
views::GridLayout::USE_PREF, 0, 0);
columns_short->AddPaddingColumn(views::GridLayout::kFixedSize,
kFieldExtraViewHorizontalPadding);
// The extra field view column is fixed size, computed from the largest
// extra view.
int short_extra_view_width =
ComputeWidestExtraViewWidth(EditorField::LengthHint::HINT_SHORT);
columns_short->AddColumn(views::GridLayout::LEADING,
views::GridLayout::CENTER,
views::GridLayout::kFixedSize,
views::GridLayout::FIXED, short_extra_view_width, 0);
// The padding at the end is fixed, computed to make sure the short field
// maintains its minimum width.
int short_padding = kDialogMinWidth - kShortFieldMinimumWidth - kLabelWidth -
(2 * kPaymentRequestRowHorizontalInsets) -
kLabelInputFieldHorizontalPadding -
kFieldExtraViewHorizontalPadding - short_extra_view_width;
columns_short->AddPaddingColumn(views::GridLayout::kFixedSize, short_padding);
// Column set for long fields.
views::ColumnSet* columns_long = editor_layout->AddColumnSet(1);
columns_long->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER,
views::GridLayout::kFixedSize,
views::GridLayout::FIXED, kLabelWidth, 0);
columns_long->AddPaddingColumn(views::GridLayout::kFixedSize,
kLabelInputFieldHorizontalPadding);
// The field view column stretches.
columns_long->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER,
1.0, views::GridLayout::USE_PREF, 0, 0);
columns_long->AddPaddingColumn(views::GridLayout::kFixedSize,
kFieldExtraViewHorizontalPadding);
// The extra field view column is fixed size, computed from the largest
// extra view.
int long_extra_view_width =
ComputeWidestExtraViewWidth(EditorField::LengthHint::HINT_LONG);
columns_long->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER,
views::GridLayout::kFixedSize,
views::GridLayout::FIXED, long_extra_view_width, 0);
// The padding at the end is fixed, computed to make sure the long field
// maintains its minimum width.
int long_padding = kDialogMinWidth - kLongFieldMinimumWidth - kLabelWidth -
(2 * kPaymentRequestRowHorizontalInsets) -
kLabelInputFieldHorizontalPadding -
kFieldExtraViewHorizontalPadding - long_extra_view_width;
columns_long->AddPaddingColumn(views::GridLayout::kFixedSize, long_padding);
views::View* first_field = nullptr;
for (const auto& field : GetFieldDefinitions()) {
bool valid = false;
views::View* focusable_field =
CreateInputField(editor_layout, 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());
views::ColumnSet* required_field_columns = editor_layout->AddColumnSet(2);
required_field_columns->AddColumn(views::GridLayout::LEADING,
views::GridLayout::CENTER, 1.0,
views::GridLayout::USE_PREF, 0, 0);
editor_layout->StartRow(views::GridLayout::kFixedSize, 2);
// Adds the "* indicates a required field" label in "hint" grey text.
editor_layout->AddView(
CreateHintLabel(
l10n_util::GetStringUTF16(IDS_PAYMENTS_REQUIRED_FIELD_MESSAGE))
.release());
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::GridLayout* layout,
const EditorField& field,
bool* valid) {
int column_set =
field.length_hint == EditorField::LengthHint::HINT_SHORT ? 0 : 1;
// This is the top padding for every row.
constexpr int kInputRowSpacing = 6;
layout->StartRowWithPadding(views::GridLayout::kFixedSize, column_set,
views::GridLayout::kFixedSize, kInputRowSpacing);
std::unique_ptr<views::Label> label = std::make_unique<views::Label>(
field.required ? field.label + base::ASCIIToUTF16("*") : field.label);
label->SetMultiLine(true);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
layout->AddView(label.release());
views::View* focusable_field = nullptr;
constexpr int kInputFieldHeight = 28;
base::string16 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();
base::string16 initial_value = GetInitialValueForType(field.type);
ValidatingTextfield* text_field =
new ValidatingTextfield(std::move(validation_delegate));
// Set the initial value and validity state.
text_field->SetText(initial_value);
text_field->SetAccessibleName(field.label);
*valid = IsEditingExistingItem() &&
delegate_ptr->IsValidTextfield(text_field, &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->set_id(GetInputFieldViewId(field.type));
text_fields_.insert(std::make_pair(text_field, field));
focusable_field = text_field;
// |text_field| will now be owned by |row|.
layout->AddView(text_field, 1.0, 1.0, views::GridLayout::FILL,
views::GridLayout::FILL, views::GridLayout::kFixedSize,
kInputFieldHeight);
break;
}
case EditorField::ControlType::COMBOBOX: {
std::unique_ptr<ValidatingCombobox> combobox =
CreateComboboxForField(field, &error_message);
focusable_field = combobox.get();
*valid = combobox->IsValid();
// |combobox| will now be owned by |row|.
layout->AddView(combobox.release(), 1.0, 1.0, views::GridLayout::FILL,
views::GridLayout::FILL, views::GridLayout::kFixedSize,
kInputFieldHeight);
break;
}
case EditorField::ControlType::CUSTOMFIELD: {
// Custom field view will now be owned by |row|. And it must be valid
// since the derived class specified a custom view for this field.
std::unique_ptr<views::View> field_view = CreateCustomFieldView(
field.type, &focusable_field, valid, &error_message);
DCHECK(field_view);
layout->AddView(field_view.release(), 1, 1, views::GridLayout::FILL,
views::GridLayout::FILL, views::GridLayout::kFixedSize,
kInputFieldHeight);
break;
}
case EditorField::ControlType::READONLY_LABEL: {
std::unique_ptr<views::Label> label =
std::make_unique<views::Label>(GetInitialValueForType(field.type));
label->set_id(GetInputFieldViewId(field.type));
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
layout->AddView(label.release(), 1, 1, views::GridLayout::FILL,
views::GridLayout::FILL, 0, 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);
if (extra_view)
layout->AddView(extra_view.release());
layout->StartRow(views::GridLayout::kFixedSize, column_set);
layout->SkipColumns(1);
std::unique_ptr<views::View> 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);
layout->AddView(error_label_view.release());
// Bottom padding for the row.
layout->AddPaddingRow(views::GridLayout::kFixedSize, kInputRowSpacing);
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::ServerFieldType type,
const base::string16& error_message) {
const auto& label_view_it = error_labels_.find(type);
DCHECK(label_view_it != error_labels_.end());
if (error_message.empty()) {
label_view_it->second->RemoveAllChildViews(/*delete_children=*/true);
} else {
if (label_view_it->second->children().empty()) {
// If there was no error label view, add it.
label_view_it->second->AddChildView(
CreateErrorLabelView(error_message, type).release());
} else {
// The error view is the only child, and has a Label as only child itself.
static_cast<views::Label*>(
label_view_it->second->child_at(0)->child_at(0))
->SetText(error_message);
}
}
}
} // namespace payments