blob: 72bbb30dfe528479b64130bc24f4376d4a34df1b [file] [log] [blame]
// Copyright 2016 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/payment_request_views_util.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/views/chrome_typography.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/grit/chromium_strings.h"
#include "chrome/grit/theme_resources.h"
#include "components/autofill/core/browser/autofill_data_util.h"
#include "components/autofill/core/browser/autofill_type.h"
#include "components/autofill/core/browser/data_model/autofill_profile.h"
#include "components/autofill/core/browser/data_model/credit_card.h"
#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/geo/phone_number_i18n.h"
#include "components/payments/content/icon/icon_size.h"
#include "components/payments/core/payment_options_provider.h"
#include "components/payments/core/payment_request_data_util.h"
#include "components/payments/core/payments_profile_comparator.h"
#include "components/payments/core/strings_util.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "third_party/blink/public/mojom/payments/payment_request.mojom.h"
#include "ui/base/default_style.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/painter.h"
#include "ui/views/view.h"
namespace payments {
namespace {
class ThemeTrackingLabel : public views::Label {
public:
METADATA_HEADER(ThemeTrackingLabel);
explicit ThemeTrackingLabel(const std::u16string& text) : Label(text) {}
~ThemeTrackingLabel() override = default;
void set_enabled_color_id(ui::ColorId enabled_color_id) {
enabled_color_id_ = enabled_color_id;
}
// views::Label:
void OnThemeChanged() override {
Label::OnThemeChanged();
if (enabled_color_id_.has_value())
SetEnabledColor(GetColorProvider()->GetColor(*enabled_color_id_));
}
private:
absl::optional<ui::ColorId> enabled_color_id_;
};
BEGIN_METADATA(ThemeTrackingLabel, views::Label)
END_METADATA
class ChromeLogoImageView : public views::ImageView {
public:
METADATA_HEADER(ChromeLogoImageView);
ChromeLogoImageView() {
SetCanProcessEventsWithinSubtree(false);
SetTooltipText(l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
}
~ChromeLogoImageView() override = default;
// views::ImageView:
void OnThemeChanged() override {
ImageView::OnThemeChanged();
SetImage(ui::ResourceBundle::GetSharedInstance()
.GetImageNamed(GetNativeTheme()->ShouldUseDarkColors()
? IDR_PRODUCT_LOGO_NAME_22_WHITE
: IDR_PRODUCT_LOGO_NAME_22)
.AsImageSkia());
}
};
BEGIN_METADATA(ChromeLogoImageView, views::ImageView)
END_METADATA
class PaymentRequestBackArrowButton : public views::ImageButton {
public:
explicit PaymentRequestBackArrowButton(
views::Button::PressedCallback back_arrow_callback)
: views::ImageButton(back_arrow_callback) {
ConfigureVectorImageButton(this);
constexpr int kBackArrowSize = 16;
SetSize(gfx::Size(kBackArrowSize, kBackArrowSize));
SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
SetID(static_cast<int>(DialogViewID::BACK_BUTTON));
SetAccessibleName(l10n_util::GetStringUTF16(IDS_PAYMENTS_BACK));
}
void OnThemeChanged() override {
views::View::OnThemeChanged();
const auto* const cp = GetColorProvider();
views::SetImageFromVectorIconWithColor(
this, vector_icons::kBackArrowIcon,
cp->GetColor(kColorPaymentsRequestBackArrowButtonIcon),
cp->GetColor(kColorPaymentsRequestBackArrowButtonIconDisabled));
}
};
// |s1|, |s2|, and |s3| are lines identifying the profile. |s1| is the
// "headline" which may be emphasized depending on |type|. If |enabled| is
// false, the labels will look disabled.
std::unique_ptr<views::View> GetBaseProfileLabel(
AddressStyleType type,
const std::u16string& s1,
const std::u16string& s2,
const std::u16string& s3,
std::u16string* accessible_content,
bool enabled = true) {
DCHECK(accessible_content);
std::unique_ptr<views::View> container = std::make_unique<views::View>();
std::unique_ptr<views::BoxLayout> layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStart);
container->SetLayoutManager(std::move(layout));
if (!s1.empty()) {
const int text_style = type == AddressStyleType::DETAILED
? static_cast<int>(STYLE_EMPHASIZED)
: static_cast<int>(views::style::STYLE_PRIMARY);
auto label = std::make_unique<ThemeTrackingLabel>(s1);
label->SetTextContext(views::style::CONTEXT_LABEL);
label->SetTextStyle(text_style);
label->SetID(static_cast<int>(DialogViewID::PROFILE_LABEL_LINE_1));
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
if (!enabled)
label->set_enabled_color_id(ui::kColorLabelForegroundDisabled);
container->AddChildView(std::move(label));
}
if (!s2.empty()) {
auto label = std::make_unique<ThemeTrackingLabel>(s2);
label->SetID(static_cast<int>(DialogViewID::PROFILE_LABEL_LINE_2));
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
if (!enabled)
label->set_enabled_color_id(ui::kColorLabelForegroundDisabled);
container->AddChildView(std::move(label));
}
if (!s3.empty()) {
auto label = std::make_unique<ThemeTrackingLabel>(s3);
label->SetID(static_cast<int>(DialogViewID::PROFILE_LABEL_LINE_3));
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
if (!enabled)
label->set_enabled_color_id(ui::kColorLabelForegroundDisabled);
container->AddChildView(std::move(label));
}
*accessible_content = l10n_util::GetStringFUTF16(
IDS_PAYMENTS_PROFILE_LABELS_ACCESSIBLE_FORMAT, s1, s2, s3);
return container;
}
// Returns a label representing the |profile| as a shipping address. See
// GetBaseProfileLabel() for more documentation.
std::unique_ptr<views::View> GetShippingAddressLabel(
AddressStyleType type,
const std::string& locale,
const autofill::AutofillProfile& profile,
std::u16string* accessible_content,
bool enabled) {
DCHECK(accessible_content);
std::u16string name = profile.GetInfo(autofill::NAME_FULL, locale);
std::u16string address =
GetShippingAddressLabelFromAutofillProfile(profile, locale);
std::u16string phone =
autofill::i18n::GetFormattedPhoneNumberForDisplay(profile, locale);
return GetBaseProfileLabel(type, name, address, phone, accessible_content,
enabled);
}
std::unique_ptr<views::Label> GetLabelForMissingInformation(
const std::u16string& missing_info) {
auto label = std::make_unique<ThemeTrackingLabel>(missing_info);
label->SetTextContext(CONTEXT_DIALOG_BODY_TEXT_SMALL);
label->SetID(static_cast<int>(DialogViewID::PROFILE_LABEL_ERROR));
// Missing information typically has a nice shade of blue.
label->set_enabled_color_id(ui::kColorLinkForeground);
return label;
}
// Paints the gray horizontal line that doesn't span the entire width of the
// dialog at the bottom of the view it borders.
class PaymentRequestRowBorderPainter : public views::Painter {
public:
explicit PaymentRequestRowBorderPainter(SkColor color) : color_(color) {}
PaymentRequestRowBorderPainter(const PaymentRequestRowBorderPainter&) =
delete;
PaymentRequestRowBorderPainter& operator=(
const PaymentRequestRowBorderPainter&) = delete;
~PaymentRequestRowBorderPainter() override {}
// views::Painter:
gfx::Size GetMinimumSize() const override {
return gfx::Size(2 * payments::kPaymentRequestRowHorizontalInsets, 1);
}
void Paint(gfx::Canvas* canvas, const gfx::Size& size) override {
int line_height = size.height() - 1;
canvas->DrawLine(
gfx::PointF(payments::kPaymentRequestRowHorizontalInsets, line_height),
gfx::PointF(size.width() - payments::kPaymentRequestRowHorizontalInsets,
line_height),
color_);
}
private:
SkColor color_;
};
} // namespace
void PopulateSheetHeaderView(bool show_back_arrow,
std::unique_ptr<views::View> header_content_view,
views::Button::PressedCallback back_arrow_callback,
views::View* container,
std::unique_ptr<views::Background> background) {
container->SetBackground(std::move(background));
views::BoxLayout* layout =
container->SetLayoutManager(std::make_unique<views::BoxLayout>());
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
// Need some spacing if the optional back arrow presents.
constexpr int kPaddingBetweenArrowAndTitle = 8;
layout->set_between_child_spacing(kPaddingBetweenArrowAndTitle);
constexpr int kVerticalInset = 14;
constexpr int kHeaderHorizontalInset = 16;
container->SetBorder(views::CreateEmptyBorder(
gfx::Insets::TLBR(kVerticalInset, kHeaderHorizontalInset, kVerticalInset,
kHeaderHorizontalInset)));
if (show_back_arrow)
container->AddChildView(
std::make_unique<PaymentRequestBackArrowButton>(back_arrow_callback));
layout->SetFlexForView(
container->AddChildView(std::move(header_content_view)), 1);
}
std::unique_ptr<views::ImageView> CreateAppIconView(
int icon_resource_id,
const SkBitmap* icon_bitmap,
const std::u16string& tooltip_text,
float opacity) {
std::unique_ptr<views::ImageView> icon_view =
std::make_unique<views::ImageView>();
icon_view->SetCanProcessEventsWithinSubtree(false);
if (icon_bitmap || !icon_resource_id) {
gfx::ImageSkia img = gfx::ImageSkia::CreateFrom1xBitmap(
(icon_bitmap ? *icon_bitmap : SkBitmap()))
.DeepCopy();
icon_view->SetImage(img);
float width = base::checked_cast<float>(img.width());
float height = base::checked_cast<float>(img.height());
float ratio = 1;
if (width && height)
ratio = width / height;
// We should set image size in density indepent pixels here, since
// views::ImageView objects are rastered at the device scale factor.
icon_view->SetImageSize(gfx::Size(
ratio * IconSizeCalculator::kPaymentAppDeviceIndependentIdealIconHeight,
IconSizeCalculator::kPaymentAppDeviceIndependentIdealIconHeight));
} else {
icon_view->SetImage(ui::ResourceBundle::GetSharedInstance()
.GetImageNamed(icon_resource_id)
.AsImageSkia());
// Images from |icon_resource_id| are 32x20 credit cards.
icon_view->SetImageSize(gfx::Size(32, 20));
}
icon_view->SetTooltipText(tooltip_text);
icon_view->SetPaintToLayer();
icon_view->layer()->SetFillsBoundsOpaquely(false);
icon_view->layer()->SetOpacity(opacity);
return icon_view;
}
std::unique_ptr<views::View> CreateProductLogoFooterView() {
std::unique_ptr<views::View> content_view = std::make_unique<views::View>();
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(), 0);
layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kStart);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStart);
content_view->SetLayoutManager(std::move(layout));
// Adds the Chrome logo image.
content_view->AddChildView(std::make_unique<ChromeLogoImageView>());
return content_view;
}
std::unique_ptr<views::View> GetShippingAddressLabelWithMissingInfo(
AddressStyleType type,
const std::string& locale,
const autofill::AutofillProfile& profile,
const PaymentsProfileComparator& comp,
std::u16string* accessible_content,
bool enabled) {
DCHECK(accessible_content);
std::unique_ptr<views::View> base_label = GetShippingAddressLabel(
type, locale, profile, accessible_content, enabled);
std::u16string missing = comp.GetStringForMissingShippingFields(profile);
if (!missing.empty()) {
base_label->AddChildView(GetLabelForMissingInformation(missing));
*accessible_content = l10n_util::GetStringFUTF16(
IDS_PAYMENTS_ACCESSIBLE_LABEL_WITH_ERROR, *accessible_content, missing);
}
return base_label;
}
// TODO(anthonyvd): unit test the label layout.
std::unique_ptr<views::View> GetContactInfoLabel(
AddressStyleType type,
const std::string& locale,
const autofill::AutofillProfile& profile,
bool request_payer_name,
bool request_payer_email,
bool request_payer_phone,
const PaymentsProfileComparator& comp,
std::u16string* accessible_content) {
DCHECK(accessible_content);
std::u16string name = request_payer_name
? profile.GetInfo(autofill::NAME_FULL, locale)
: std::u16string();
std::u16string phone =
request_payer_phone
? autofill::i18n::GetFormattedPhoneNumberForDisplay(profile, locale)
: std::u16string();
std::u16string email = request_payer_email
? profile.GetInfo(autofill::EMAIL_ADDRESS, locale)
: std::u16string();
std::unique_ptr<views::View> base_label =
GetBaseProfileLabel(type, name, phone, email, accessible_content);
std::u16string missing = comp.GetStringForMissingContactFields(profile);
if (!missing.empty()) {
base_label->AddChildView(GetLabelForMissingInformation(missing));
*accessible_content = l10n_util::GetStringFUTF16(
IDS_PAYMENTS_ACCESSIBLE_LABEL_WITH_ERROR, *accessible_content, missing);
}
return base_label;
}
std::unique_ptr<views::Border> CreatePaymentRequestRowBorder(
SkColor color,
const gfx::Insets& insets) {
return views::CreateBorderPainter(
std::make_unique<PaymentRequestRowBorderPainter>(color), insets);
}
std::unique_ptr<views::Label> CreateBoldLabel(const std::u16string& text) {
return std::make_unique<views::Label>(text, views::style::CONTEXT_LABEL,
STYLE_EMPHASIZED);
}
std::unique_ptr<views::Label> CreateMediumLabel(const std::u16string& text) {
// TODO(tapted): This should refer to a style in the Chrome typography spec.
// Also, it needs to handle user setups where the default font is BOLD already
// since asking for a MEDIUM font will give a lighter font.
std::unique_ptr<views::Label> label = std::make_unique<views::Label>(text);
label->SetFontList(
ui::ResourceBundle::GetSharedInstance().GetFontListForDetails(
ui::ResourceBundle::FontDetails(std::string(),
ui::kLabelFontSizeDelta,
gfx::Font::Weight::MEDIUM)));
return label;
}
std::unique_ptr<views::Label> CreateHintLabel(
const std::u16string& text,
gfx::HorizontalAlignment alignment) {
std::unique_ptr<views::Label> label = std::make_unique<views::Label>(
text, views::style::CONTEXT_LABEL, views::style::STYLE_HINT);
label->SetHorizontalAlignment(alignment);
return label;
}
std::unique_ptr<views::View> CreateShippingOptionLabel(
payments::mojom::PaymentShippingOption* shipping_option,
const std::u16string& formatted_amount,
bool emphasize_label,
std::u16string* accessible_content) {
DCHECK(accessible_content);
std::unique_ptr<views::View> container = std::make_unique<views::View>();
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStart);
container->SetLayoutManager(std::move(layout));
if (shipping_option) {
const std::u16string& text = base::UTF8ToUTF16(shipping_option->label);
std::unique_ptr<views::Label> shipping_label =
emphasize_label ? CreateMediumLabel(text)
: std::make_unique<views::Label>(text);
// Strings from the website may not match the locale of the device, so align
// them according to the language of the text. This will result, for
// example, in "he" labels being right-aligned in a browser that's using
// "en" locale.
shipping_label->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
shipping_label->SetID(
static_cast<int>(DialogViewID::SHIPPING_OPTION_DESCRIPTION));
container->AddChildView(std::move(shipping_label));
std::unique_ptr<views::Label> amount_label =
std::make_unique<views::Label>(formatted_amount);
amount_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
amount_label->SetID(static_cast<int>(DialogViewID::SHIPPING_OPTION_AMOUNT));
container->AddChildView(std::move(amount_label));
*accessible_content = l10n_util::GetStringFUTF16(
IDS_PAYMENTS_PROFILE_LABELS_ACCESSIBLE_FORMAT, text, formatted_amount,
std::u16string());
}
return container;
}
std::unique_ptr<views::View> CreateWarningView(const std::u16string& message,
bool show_icon) {
auto header_view = std::make_unique<views::View>();
// 8 pixels between the warning icon view (if present) and the text.
constexpr int kRowHorizontalSpacing = 8;
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets::VH(0, kPaymentRequestRowHorizontalInsets),
kRowHorizontalSpacing);
layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kStart);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStretch);
header_view->SetLayoutManager(std::move(layout));
auto label = std::make_unique<ThemeTrackingLabel>(message);
// If the warning message comes from the websites, then align label
// according to the language of the website's text.
label->SetHorizontalAlignment(message.empty() ? gfx::ALIGN_LEFT
: gfx::ALIGN_TO_HEAD);
label->SetID(static_cast<int>(DialogViewID::WARNING_LABEL));
label->SetMultiLine(true);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
if (show_icon) {
auto warning_icon = std::make_unique<views::ImageView>();
warning_icon->SetCanProcessEventsWithinSubtree(false);
warning_icon->SetImage(ui::ImageModel::FromVectorIcon(
vector_icons::kWarningIcon, ui::kColorAlertHighSeverity, 16));
header_view->AddChildView(std::move(warning_icon));
label->set_enabled_color_id(ui::kColorAlertHighSeverity);
}
header_view->AddChildView(std::move(label));
return header_view;
}
} // namespace payments