1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/views/autofill/card_unmask_prompt_views.h"
7 #include "base/basictypes.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "chrome/browser/ui/autofill/autofill_dialog_types.h"
10 #include "chrome/browser/ui/autofill/card_unmask_prompt_controller.h"
11 #include "chrome/browser/ui/views/autofill/decorated_textfield.h"
12 #include "chrome/browser/ui/views/autofill/tooltip_icon.h"
13 #include "chrome/grit/generated_resources.h"
14 #include "components/constrained_window/constrained_window_views.h"
15 #include "components/web_modal/web_contents_modal_dialog_host.h"
16 #include "components/web_modal/web_contents_modal_dialog_manager.h"
17 #include "components/web_modal/web_contents_modal_dialog_manager_delegate.h"
18 #include "grit/theme_resources.h"
19 #include "third_party/skia/include/core/SkColor.h"
20 #include "ui/base/l10n/l10n_util.h"
21 #include "ui/base/resource/resource_bundle.h"
22 #include "ui/gfx/canvas.h"
23 #include "ui/views/background.h"
24 #include "ui/views/controls/button/checkbox.h"
25 #include "ui/views/controls/combobox/combobox.h"
26 #include "ui/views/controls/image_view.h"
27 #include "ui/views/controls/label.h"
28 #include "ui/views/controls/throbber.h"
29 #include "ui/views/layout/box_layout.h"
30 #include "ui/views/widget/widget.h"
31 #include "ui/views/window/dialog_client_view.h"
35 // The number of pixels of blank space on the outer horizontal edges of the
37 const int kEdgePadding
= 19;
39 SkColor kGreyTextColor
= SkColorSetRGB(0x64, 0x64, 0x64);
42 CardUnmaskPromptView
* CardUnmaskPromptView::CreateAndShow(
43 CardUnmaskPromptController
* controller
) {
44 CardUnmaskPromptViews
* view
= new CardUnmaskPromptViews(controller
);
49 CardUnmaskPromptViews::CardUnmaskPromptViews(
50 CardUnmaskPromptController
* controller
)
51 : controller_(controller
),
52 main_contents_(nullptr),
53 permanent_error_label_(nullptr),
56 month_input_(nullptr),
59 error_label_(nullptr),
60 storage_row_(nullptr),
61 storage_checkbox_(nullptr),
62 progress_overlay_(nullptr),
63 progress_throbber_(nullptr),
64 progress_label_(nullptr),
65 overlay_animation_(this),
66 weak_ptr_factory_(this) {
69 CardUnmaskPromptViews::~CardUnmaskPromptViews() {
71 controller_
->OnUnmaskDialogClosed();
74 void CardUnmaskPromptViews::Show() {
75 constrained_window::ShowWebModalDialogViews(this,
76 controller_
->GetWebContents());
79 void CardUnmaskPromptViews::ControllerGone() {
80 controller_
= nullptr;
84 void CardUnmaskPromptViews::DisableAndWaitForVerification() {
85 SetInputsEnabled(false);
86 progress_overlay_
->SetOpacity(0.0);
87 progress_overlay_
->SetVisible(true);
88 progress_throbber_
->Start();
89 overlay_animation_
.Show();
90 GetDialogClientView()->UpdateDialogButtons();
94 void CardUnmaskPromptViews::GotVerificationResult(
95 const base::string16
& error_message
,
97 progress_throbber_
->Stop();
98 if (error_message
.empty()) {
99 progress_label_
->SetText(l10n_util::GetStringUTF16(
100 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_SUCCESS
));
101 progress_throbber_
->SetChecked(true);
102 base::MessageLoop::current()->PostDelayedTask(
103 FROM_HERE
, base::Bind(&CardUnmaskPromptViews::ClosePrompt
,
104 weak_ptr_factory_
.GetWeakPtr()),
105 controller_
->GetSuccessMessageDuration());
107 // TODO(estade): it's somewhat jarring when the error comes back too
109 overlay_animation_
.Reset();
110 storage_row_
->SetOpacity(1.0);
111 progress_overlay_
->SetVisible(false);
114 SetInputsEnabled(true);
116 // If there is more than one input showing, don't mark anything as
117 // invalid since we don't know the location of the problem.
118 if (!controller_
->ShouldRequestExpirationDate())
119 cvc_input_
->SetInvalid(true);
121 // TODO(estade): When do we hide |error_label_|?
122 SetRetriableErrorMessage(error_message
);
124 permanent_error_label_
->SetText(error_message
);
125 permanent_error_label_
->SetVisible(true);
126 SetRetriableErrorMessage(base::string16());
128 GetDialogClientView()->UpdateDialogButtons();
134 void CardUnmaskPromptViews::SetRetriableErrorMessage(
135 const base::string16
& message
) {
136 if (message
.empty()) {
137 error_label_
->SetMultiLine(false);
138 error_label_
->SetText(base::ASCIIToUTF16(" "));
139 error_icon_
->SetVisible(false);
141 error_label_
->SetMultiLine(true);
142 error_label_
->SetText(message
);
143 error_icon_
->SetVisible(true);
146 // Update the dialog's size.
147 if (GetWidget() && controller_
->GetWebContents()) {
148 constrained_window::UpdateWebContentsModalDialogPosition(
149 GetWidget(), web_modal::WebContentsModalDialogManager::FromWebContents(
150 controller_
->GetWebContents())
152 ->GetWebContentsModalDialogHost());
158 void CardUnmaskPromptViews::SetInputsEnabled(bool enabled
) {
159 cvc_input_
->SetEnabled(enabled
);
160 if (storage_checkbox_
)
161 storage_checkbox_
->SetEnabled(enabled
);
163 month_input_
->SetEnabled(enabled
);
165 year_input_
->SetEnabled(enabled
);
168 views::View
* CardUnmaskPromptViews::GetContentsView() {
173 views::View
* CardUnmaskPromptViews::CreateFootnoteView() {
174 if (!controller_
->CanStoreLocally())
177 // Local storage checkbox and (?) tooltip.
178 storage_row_
= new FadeOutView();
179 views::BoxLayout
* storage_row_layout
= new views::BoxLayout(
180 views::BoxLayout::kHorizontal
, kEdgePadding
, kEdgePadding
, 0);
181 storage_row_
->SetLayoutManager(storage_row_layout
);
182 storage_row_
->SetBorder(
183 views::Border::CreateSolidSidedBorder(1, 0, 0, 0, kSubtleBorderColor
));
184 storage_row_
->set_background(
185 views::Background::CreateSolidBackground(kLightShadingColor
));
187 storage_checkbox_
= new views::Checkbox(l10n_util::GetStringUTF16(
188 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_CHECKBOX
));
189 storage_checkbox_
->SetChecked(controller_
->GetStoreLocallyStartState());
190 storage_row_
->AddChildView(storage_checkbox_
);
191 storage_row_layout
->SetFlexForView(storage_checkbox_
, 1);
193 storage_row_
->AddChildView(new TooltipIcon(l10n_util::GetStringUTF16(
194 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_TOOLTIP
)));
199 gfx::Size
CardUnmaskPromptViews::GetPreferredSize() const {
200 // Must hardcode a width so the label knows where to wrap.
201 const int kWidth
= 375;
202 return gfx::Size(kWidth
, GetHeightForWidth(kWidth
));
205 void CardUnmaskPromptViews::Layout() {
206 gfx::Rect contents_bounds
= GetContentsBounds();
207 main_contents_
->SetBoundsRect(contents_bounds
);
209 // The progress overlay extends from the top of the input row
210 // to the bottom of the content area.
211 gfx::RectF input_rect
= input_row_
->GetContentsBounds();
212 View::ConvertRectToTarget(input_row_
, this, &input_rect
);
213 input_rect
.set_height(contents_bounds
.height());
214 contents_bounds
.Intersect(gfx::ToNearestRect(input_rect
));
215 progress_overlay_
->SetBoundsRect(contents_bounds
);
218 int CardUnmaskPromptViews::GetHeightForWidth(int width
) const {
221 const gfx::Insets insets
= GetInsets();
222 return main_contents_
->GetHeightForWidth(width
- insets
.width()) +
226 void CardUnmaskPromptViews::OnNativeThemeChanged(const ui::NativeTheme
* theme
) {
228 theme
->GetSystemColor(ui::NativeTheme::kColorId_DialogBackground
);
229 progress_overlay_
->set_background(
230 views::Background::CreateSolidBackground(bg_color
));
231 progress_label_
->SetBackgroundColor(bg_color
);
234 ui::ModalType
CardUnmaskPromptViews::GetModalType() const {
235 return ui::MODAL_TYPE_CHILD
;
238 base::string16
CardUnmaskPromptViews::GetWindowTitle() const {
239 return controller_
->GetWindowTitle();
242 void CardUnmaskPromptViews::DeleteDelegate() {
246 int CardUnmaskPromptViews::GetDialogButtons() const {
247 return ui::DIALOG_BUTTON_OK
| ui::DIALOG_BUTTON_CANCEL
;
250 base::string16
CardUnmaskPromptViews::GetDialogButtonLabel(
251 ui::DialogButton button
) const {
252 if (button
== ui::DIALOG_BUTTON_OK
)
253 return l10n_util::GetStringUTF16(IDS_AUTOFILL_CARD_UNMASK_CONFIRM_BUTTON
);
255 return DialogDelegateView::GetDialogButtonLabel(button
);
258 bool CardUnmaskPromptViews::ShouldDefaultButtonBeBlue() const {
262 bool CardUnmaskPromptViews::IsDialogButtonEnabled(
263 ui::DialogButton button
) const {
264 if (button
== ui::DIALOG_BUTTON_CANCEL
)
267 DCHECK_EQ(ui::DIALOG_BUTTON_OK
, button
);
269 return cvc_input_
->enabled() &&
270 controller_
->InputCvcIsValid(cvc_input_
->text()) &&
271 ExpirationDateIsValid();
274 views::View
* CardUnmaskPromptViews::GetInitiallyFocusedView() {
278 bool CardUnmaskPromptViews::Cancel() {
282 bool CardUnmaskPromptViews::Accept() {
286 controller_
->OnUnmaskResponse(
288 month_input_
? month_input_
->GetTextForRow(month_input_
->selected_index())
290 year_input_
? year_input_
->GetTextForRow(year_input_
->selected_index())
292 storage_checkbox_
? storage_checkbox_
->checked() : false);
296 void CardUnmaskPromptViews::ContentsChanged(
297 views::Textfield
* sender
,
298 const base::string16
& new_contents
) {
299 if (controller_
->InputCvcIsValid(new_contents
))
300 cvc_input_
->SetInvalid(false);
302 GetDialogClientView()->UpdateDialogButtons();
305 void CardUnmaskPromptViews::OnPerformAction(views::Combobox
* combobox
) {
306 if (ExpirationDateIsValid()) {
307 if (month_input_
->invalid()) {
308 month_input_
->SetInvalid(false);
309 year_input_
->SetInvalid(false);
310 SetRetriableErrorMessage(base::string16());
312 } else if (month_input_
->selected_index() !=
313 month_combobox_model_
.GetDefaultIndex() &&
314 year_input_
->selected_index() !=
315 year_combobox_model_
.GetDefaultIndex()) {
316 month_input_
->SetInvalid(true);
317 year_input_
->SetInvalid(true);
318 SetRetriableErrorMessage(l10n_util::GetStringUTF16(
319 IDS_AUTOFILL_CARD_UNMASK_INVALID_EXPIRATION_DATE
));
322 GetDialogClientView()->UpdateDialogButtons();
325 void CardUnmaskPromptViews::AnimationProgressed(
326 const gfx::Animation
* animation
) {
327 progress_overlay_
->SetOpacity(animation
->GetCurrentValue());
328 storage_row_
->SetOpacity(1.0 - animation
->GetCurrentValue());
331 void CardUnmaskPromptViews::InitIfNecessary() {
335 main_contents_
= new views::View();
336 main_contents_
->SetLayoutManager(
337 new views::BoxLayout(views::BoxLayout::kVertical
, 0, 0, 12));
338 AddChildView(main_contents_
);
340 permanent_error_label_
= new views::Label();
341 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
342 permanent_error_label_
->SetFontList(
343 rb
.GetFontList(ui::ResourceBundle::BoldFont
));
344 permanent_error_label_
->set_background(
345 views::Background::CreateSolidBackground(kWarningColor
));
346 permanent_error_label_
->SetBorder(
347 views::Border::CreateEmptyBorder(12, kEdgePadding
, 12, kEdgePadding
));
348 permanent_error_label_
->SetEnabledColor(SK_ColorWHITE
);
349 permanent_error_label_
->SetAutoColorReadabilityEnabled(false);
350 permanent_error_label_
->SetVisible(false);
351 permanent_error_label_
->SetMultiLine(true);
352 permanent_error_label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
353 main_contents_
->AddChildView(permanent_error_label_
);
355 views::View
* controls_container
= new views::View();
356 controls_container
->SetLayoutManager(
357 new views::BoxLayout(views::BoxLayout::kVertical
, kEdgePadding
, 0, 0));
358 main_contents_
->AddChildView(controls_container
);
360 views::Label
* instructions
=
361 new views::Label(controller_
->GetInstructionsMessage());
362 instructions
->SetEnabledColor(kGreyTextColor
);
363 instructions
->SetMultiLine(true);
364 instructions
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
365 instructions
->SetBorder(views::Border::CreateEmptyBorder(0, 0, 16, 0));
366 controls_container
->AddChildView(instructions
);
368 input_row_
= new views::View();
369 input_row_
->SetLayoutManager(
370 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 5));
371 controls_container
->AddChildView(input_row_
);
373 if (controller_
->ShouldRequestExpirationDate()) {
374 month_input_
= new views::Combobox(&month_combobox_model_
);
375 month_input_
->set_listener(this);
376 input_row_
->AddChildView(month_input_
);
377 views::Label
* separator
= new views::Label(l10n_util::GetStringUTF16(
378 IDS_AUTOFILL_CARD_UNMASK_EXPIRATION_DATE_SEPARATOR
));
379 separator
->SetEnabledColor(kGreyTextColor
);
380 input_row_
->AddChildView(separator
);
381 year_input_
= new views::Combobox(&year_combobox_model_
);
382 year_input_
->set_listener(this);
383 input_row_
->AddChildView(year_input_
);
384 input_row_
->AddChildView(new views::Label(base::ASCIIToUTF16(" ")));
387 cvc_input_
= new DecoratedTextfield(
389 l10n_util::GetStringUTF16(IDS_AUTOFILL_DIALOG_PLACEHOLDER_CVC
), this);
390 cvc_input_
->set_default_width_in_chars(8);
391 input_row_
->AddChildView(cvc_input_
);
393 views::ImageView
* cvc_image
= new views::ImageView();
394 cvc_image
->SetImage(rb
.GetImageSkiaNamed(controller_
->GetCvcImageRid()));
395 input_row_
->AddChildView(cvc_image
);
397 views::View
* temporary_error
= new views::View();
398 views::BoxLayout
* temporary_error_layout
=
399 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 4);
400 temporary_error
->SetLayoutManager(temporary_error_layout
);
401 temporary_error_layout
->set_cross_axis_alignment(
402 views::BoxLayout::CROSS_AXIS_ALIGNMENT_START
);
403 temporary_error
->SetBorder(views::Border::CreateEmptyBorder(8, 0, 0, 0));
404 controls_container
->AddChildView(temporary_error
);
406 error_icon_
= new views::ImageView();
407 error_icon_
->SetVisible(false);
408 error_icon_
->SetImage(ui::ResourceBundle::GetSharedInstance()
409 .GetImageNamed(IDR_ALERT_RED
)
411 temporary_error
->AddChildView(error_icon_
);
413 // Reserve vertical space for the error label, assuming it's one line.
414 error_label_
= new views::Label(base::ASCIIToUTF16(" "));
415 error_label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
416 error_label_
->SetEnabledColor(kWarningColor
);
417 temporary_error
->AddChildView(error_label_
);
418 temporary_error_layout
->SetFlexForView(error_label_
, 1);
420 progress_overlay_
= new FadeOutView();
421 progress_overlay_
->set_fade_everything(true);
422 views::BoxLayout
* progress_layout
=
423 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 5);
424 progress_layout
->set_cross_axis_alignment(
425 views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER
);
426 progress_layout
->set_main_axis_alignment(
427 views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER
);
428 progress_overlay_
->SetLayoutManager(progress_layout
);
430 progress_overlay_
->SetVisible(false);
431 AddChildView(progress_overlay_
);
433 progress_throbber_
= new views::CheckmarkThrobber();
434 progress_overlay_
->AddChildView(progress_throbber_
);
436 progress_label_
= new views::Label(l10n_util::GetStringUTF16(
437 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_IN_PROGRESS
));
438 // Material blue. TODO(estade): find an appropriate place for this color.
439 progress_label_
->SetEnabledColor(SkColorSetRGB(0x42, 0x85, 0xF4));
440 progress_overlay_
->AddChildView(progress_label_
);
443 bool CardUnmaskPromptViews::ExpirationDateIsValid() const {
444 if (!controller_
->ShouldRequestExpirationDate())
447 return controller_
->InputExpirationIsValid(
448 month_input_
->GetTextForRow(month_input_
->selected_index()),
449 year_input_
->GetTextForRow(year_input_
->selected_index()));
452 void CardUnmaskPromptViews::ClosePrompt() {
453 GetWidget()->Close();
456 CardUnmaskPromptViews::FadeOutView::FadeOutView()
457 : fade_everything_(false), opacity_(1.0) {
459 CardUnmaskPromptViews::FadeOutView::~FadeOutView() {
462 void CardUnmaskPromptViews::FadeOutView::PaintChildren(
463 const PaintContext
& context
) {
465 return views::View::PaintChildren(context
);
467 gfx::Canvas
* canvas
= context
.canvas();
468 canvas
->SaveLayerAlpha(0xff * opacity_
);
469 views::View::PaintChildren(context
);
473 void CardUnmaskPromptViews::FadeOutView::OnPaint(gfx::Canvas
* canvas
) {
474 if (!fade_everything_
|| opacity_
> 0.99)
475 return views::View::OnPaint(canvas
);
477 canvas
->SaveLayerAlpha(0xff * opacity_
);
478 views::View::OnPaint(canvas
);
482 void CardUnmaskPromptViews::FadeOutView::SetOpacity(double opacity
) {
487 } // namespace autofill