blob: bf222ac0b8be6a1f027cdbab35bde26b998c7180 [file] [log] [blame]
// Copyright 2021 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/autofill/address_editor_view.h"
#include <cstddef>
#include <memory>
#include <string_view>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "components/autofill/core/browser/geo/autofill_country.h"
#include "components/autofill/core/browser/ui/country_combobox_model.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/combobox/combobox.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/box_layout_view.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/view.h"
#include "ui/views/window/dialog_delegate.h"
namespace autofill {
namespace {
// Returns the View ID that can be used to lookup the input field for |type|.
int GetInputFieldViewId(autofill::FieldType type) {
return static_cast<int>(type);
}
} // namespace
AddressEditorView::AddressEditorView(
std::unique_ptr<AddressEditorController> controller)
: controller_(std::move(controller)) {
CreateEditorView();
}
AddressEditorView::~AddressEditorView() = default;
void AddressEditorView::PreferredSizeChanged() {
views::View::PreferredSizeChanged();
SizeToPreferredSize();
}
const autofill::AutofillProfile& AddressEditorView::GetAddressProfile() {
if (controller_->is_validatable()) {
ValidateAllFields();
CHECK(controller_->is_valid().has_value() && *controller_->is_valid())
<< "The editor doesn't return an invalid profile, check "
"`AddressEditorController::is_valid()` before calling this method.";
}
SaveFieldsToProfile();
return controller_->GetAddressProfile();
}
bool AddressEditorView::ValidateAllFields() {
if (!controller_->is_validatable()) {
return true;
}
all_address_fields_have_been_validated_ = true;
int number_of_invalid_fields = 0;
for (const auto& field : text_fields_) {
bool is_field_invalid =
!controller_->IsValid(field.second, field.first->GetText());
field.first->SetInvalid(is_field_invalid);
number_of_invalid_fields += is_field_invalid;
}
bool is_valid = number_of_invalid_fields == 0;
controller_->SetIsValid(is_valid);
std::u16string validation_error;
if (number_of_invalid_fields == 1) {
validation_error = l10n_util::GetStringUTF16(
IDS_AUTOFILL_EDIT_ADDRESS_REQUIRED_FIELD_FORM_ERROR);
} else if (number_of_invalid_fields > 1) {
validation_error = l10n_util::GetStringUTF16(
IDS_AUTOFILL_EDIT_ADDRESS_REQUIRED_FIELDS_FORM_ERROR);
}
validation_error_->SetText(validation_error);
return is_valid;
}
void AddressEditorView::SelectCountryForTesting(const std::u16string& country) {
auto* combobox = static_cast<views::Combobox*>(
GetViewByID(GetInputFieldViewId(autofill::ADDRESS_HOME_COUNTRY)));
CHECK(combobox->SelectValue(country));
OnSelectedCountryChanged(combobox);
UpdateEditorView();
}
void AddressEditorView::SetTextInputFieldValueForTesting(
autofill::FieldType type,
const std::u16string& value) {
views::Textfield* text_field =
static_cast<views::Textfield*>(GetViewByID(GetInputFieldViewId(type)));
text_field->SetText(value);
}
std::u16string_view AddressEditorView::GetValidationErrorForTesting() const {
return validation_error_ ? validation_error_->GetText()
: std::u16string_view();
}
void AddressEditorView::CreateEditorView() {
text_fields_.clear();
field_change_callbacks_.clear();
const int kBetweenChildSpacing =
ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_CONTROL_LIST_VERTICAL);
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
gfx::Insets::VH(kBetweenChildSpacing / 2, 0), kBetweenChildSpacing));
views::View* first_field = nullptr;
for (const auto& field : controller_->editor_fields()) {
views::View* view = CreateInputField(field);
if (first_field == nullptr) {
first_field = view;
}
}
initial_focus_view_ = first_field;
if (controller_->is_validatable()) {
validation_error_ =
AddChildView(views::Builder<views::Label>()
.SetMultiLine(true)
.SetEnabledColor(ui::kColorAlertHighSeverity)
.SetHorizontalAlignment(gfx::ALIGN_LEFT)
.Build());
}
}
// Field views have a width of 196/260dp (short/long fields) as per spec.
// __________________________________
// |Label | 16dp pad | Field (flex) |
// |______|__________|______________|
//
// Each input field is a 2 cells.
// +----------------------------------------------------------+
// | Field Label | Input field (textfield/combobox) |
// +----------------------------------------------------------+
views::View* AddressEditorView::CreateInputField(const EditorField& field) {
constexpr int kLabelWidth = 140;
// This is the horizontal padding between the label and the field.
constexpr int kLabelInputFieldHorizontalPadding = 16;
constexpr int kShortFieldWidth = 196;
constexpr int kLongFieldWidth = 260;
constexpr int kInputFieldHeight = 28;
views::Label* label;
views::BoxLayoutView* field_layout = AddChildView(
views::Builder<views::BoxLayoutView>()
.SetBetweenChildSpacing(kLabelInputFieldHorizontalPadding)
.AddChildren(views::Builder<views::Label>()
.CopyAddressTo(&label)
.SetText(field.label)
.SetMultiLine(true)
.SetHorizontalAlignment(gfx::ALIGN_LEFT))
.Build());
label->SizeToFit(kLabelWidth);
views::View* focusable_field = nullptr;
switch (field.control_type) {
case EditorField::ControlType::TEXTFIELD:
case EditorField::ControlType::TEXTFIELD_NUMBER: {
std::u16string initial_value = controller_->GetProfileInfo(field.type);
auto text_field = std::make_unique<views::Textfield>();
// Set the initial value and validity state.
text_field->SetText(initial_value);
text_field->GetViewAccessibility().SetName(field.label);
if (field.control_type == EditorField::ControlType::TEXTFIELD_NUMBER) {
text_field->SetTextInputType(ui::TextInputType::TEXT_INPUT_TYPE_NUMBER);
}
// Using autofill field type as a view ID (for testing).
text_field->SetID(GetInputFieldViewId(field.type));
field_change_callbacks_.push_back(text_field->AddTextChangedCallback(
base::BindRepeating(&AddressEditorView::ValidateField,
base::Unretained(this), text_field.get())));
text_fields_.insert(std::make_pair(text_field.get(), field));
field.length_hint == EditorField::LengthHint::HINT_SHORT
? text_field->SetPreferredSize(
gfx::Size(kShortFieldWidth, kInputFieldHeight))
: text_field->SetPreferredSize(
gfx::Size(kLongFieldWidth, kInputFieldHeight));
// |text_field| will now be owned by |row|.
focusable_field = field_layout->AddChildView(std::move(text_field));
break;
}
case EditorField::ControlType::COMBOBOX: {
DCHECK_EQ(field.type, autofill::ADDRESS_HOME_COUNTRY);
std::unique_ptr<views::Combobox> combobox =
CreateCountryCombobox(field.label);
// |combobox| will now be owned by |row|.
focusable_field = field_layout->AddChildView(std::move(combobox));
break;
}
}
return focusable_field;
}
std::unique_ptr<views::Combobox> AddressEditorView::CreateCountryCombobox(
const std::u16string& label) {
auto& combobox_model = controller_->GetCountryComboboxModel();
auto combobox = std::make_unique<views::Combobox>(&combobox_model);
combobox->GetViewAccessibility().SetName(label);
std::u16string initial_value =
controller_->GetProfileInfo(autofill::ADDRESS_HOME_COUNTRY);
// TODO(crbug.com/40277889): check if it's possible that address country is
// not in the combobox value list.
if (!combobox->SelectValue(initial_value)) {
combobox->SelectValue(
combobox_model.GetItemAt(combobox_model.GetDefaultIndex().value()));
}
// Using autofill field type as a view ID.
combobox->SetID(GetInputFieldViewId(autofill::ADDRESS_HOME_COUNTRY));
field_change_callbacks_.push_back(combobox->AddSelectedIndexChangedCallback(
base::BindRepeating(&AddressEditorView::OnSelectedCountryChanged,
base::Unretained(this), combobox.get())));
return combobox;
}
void AddressEditorView::UpdateEditorView() {
validation_error_ = nullptr;
initial_focus_view_ = nullptr;
RemoveAllChildViews();
CreateEditorView();
PreferredSizeChanged();
// If the editor was once fully validated (`ValidateAllFields()`), it should
// keep validating the full address on any change. It ensures the error
// messages are always consistent.
if (all_address_fields_have_been_validated_) {
ValidateAllFields();
}
if (initial_focus_view_) {
initial_focus_view_->RequestFocus();
}
}
void AddressEditorView::SaveFieldsToProfile() {
// The country must be set first, because the profile uses the country to
// interpret some of the data (e.g., phone numbers) passed to SetInfo.
views::Combobox* combobox = static_cast<views::Combobox*>(
GetViewByID(GetInputFieldViewId(autofill::ADDRESS_HOME_COUNTRY)));
// The combobox can be null when saving to temporary profile while updating
// the view.
if (combobox) {
std::u16string country(
combobox->GetTextForRow(combobox->GetSelectedIndex().value()));
controller_->SetProfileInfo(autofill::ADDRESS_HOME_COUNTRY, country);
}
for (const auto& field : text_fields_) {
controller_->SetProfileInfo(field.second.type,
std::u16string(field.first->GetText()));
}
}
void AddressEditorView::OnSelectedCountryChanged(views::Combobox* combobox) {
CHECK(combobox->GetSelectedIndex().has_value());
SaveFieldsToProfile();
size_t selected_index = combobox->GetSelectedIndex().value();
CHECK(!controller_->GetCountryComboboxModel().IsItemSeparatorAt(
selected_index));
controller_->UpdateEditorFields(controller_->GetCountryComboboxModel()
.countries()[selected_index]
->country_code());
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&AddressEditorView::UpdateEditorView,
weak_ptr_factory_.GetWeakPtr()));
}
void AddressEditorView::ValidateField(views::Textfield* textfield) {
if (!controller_->is_validatable()) {
return;
}
// If the editor was once fully validated (`ValidateAllFields()`), it should
// keep validating the full address on any change. It ensures the error
// messages are always consistent.
if (all_address_fields_have_been_validated_) {
ValidateAllFields();
return;
}
const EditorField& field = text_fields_.at(textfield);
bool is_valid = controller_->IsValid(field, textfield->GetText());
textfield->SetInvalid(!is_valid);
}
BEGIN_METADATA(AddressEditorView)
END_METADATA
} // namespace autofill