1 // Copyright (c) 2015 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.
6 #include "base/message_loop/message_loop.h"
7 #include "base/strings/sys_string_conversions.h"
8 #include "chrome/browser/ui/autofill/autofill_dialog_models.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/chrome_style.h"
12 #import "chrome/browser/ui/cocoa/autofill/autofill_pop_up_button.h"
13 #import "chrome/browser/ui/cocoa/autofill/autofill_textfield.h"
14 #import "chrome/browser/ui/cocoa/autofill/autofill_tooltip_controller.h"
15 #include "chrome/browser/ui/cocoa/autofill/card_unmask_prompt_view_bridge.h"
16 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_button.h"
17 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_control_utils.h"
18 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_custom_sheet.h"
19 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_custom_window.h"
20 #import "chrome/browser/ui/cocoa/key_equivalent_constants.h"
21 #import "chrome/browser/ui/cocoa/l10n_util.h"
22 #import "chrome/browser/ui/cocoa/spinner_view.h"
23 #include "grit/generated_resources.h"
24 #include "grit/theme_resources.h"
25 #include "skia/ext/skia_utils_mac.h"
26 #include "ui/base/cocoa/window_size_constants.h"
27 #include "ui/base/l10n/l10n_util.h"
31 const CGFloat kButtonGap = 6.0f;
32 const CGFloat kButtonsToRetriableErrorGap = 12.0f;
33 const CGFloat kCvcInputWidth = 64.0f;
34 const CGFloat kCvcInputToImageGap = 8.0f;
35 const CGFloat kDialogContentMinWidth = 210.0f;
36 const CGFloat kInputRowToInstructionsGap = 16.0f;
37 const CGFloat kInstructionsToTitleGap = 8.0f;
38 const CGFloat kPermanentErrorExteriorPadding = 12.0f;
39 const CGFloat kPermanentErrorHorizontalPadding = 16.0f;
40 const CGFloat kPermanentErrorVerticalPadding = 12.0f;
41 const CGFloat kProgressToInstructionsGap = 24.0f;
42 const CGFloat kRetriableErrorToInputRowGap = 4.0f;
43 const CGFloat kSeparatorHeight = 1.0f;
44 const CGFloat kSpinnerSize = 16.0f;
45 const CGFloat kSpinnerToProgressTextGap = 8.0f;
46 const CGFloat kYearToCvcGap = 12.0f;
48 const SkColor kPermanentErrorTextColor = SK_ColorWHITE;
49 // Material blue. TODO(bondd): share with Views version.
50 const SkColor kProgressTextColor = SkColorSetRGB(0x42, 0x85, 0xf4);
51 // TODO(bondd): Unify colors with Views version and AutofillMessageView.
52 const SkColor kShadingColor = SkColorSetRGB(0xf2, 0xf2, 0xf2);
53 const SkColor kSubtleBorderColor = SkColorSetRGB(0xdf, 0xdf, 0xdf);
60 CardUnmaskPromptView* CardUnmaskPromptView::CreateAndShow(
61 CardUnmaskPromptController* controller) {
62 return new CardUnmaskPromptViewBridge(controller);
65 #pragma mark CardUnmaskPromptViewBridge
67 CardUnmaskPromptViewBridge::CardUnmaskPromptViewBridge(
68 CardUnmaskPromptController* controller)
69 : controller_(controller), weak_ptr_factory_(this) {
70 view_controller_.reset(
71 [[CardUnmaskPromptViewCocoa alloc] initWithBridge:this]);
73 // Setup the constrained window that will show the view.
74 base::scoped_nsobject<NSWindow> window([[ConstrainedWindowCustomWindow alloc]
75 initWithContentRect:[[view_controller_ view] bounds]]);
76 [window setContentView:[view_controller_ view]];
77 base::scoped_nsobject<CustomConstrainedWindowSheet> sheet(
78 [[CustomConstrainedWindowSheet alloc] initWithCustomWindow:window]);
79 constrained_window_.reset(
80 new ConstrainedWindowMac(this, controller_->GetWebContents(), sheet));
83 CardUnmaskPromptViewBridge::~CardUnmaskPromptViewBridge() {
86 void CardUnmaskPromptViewBridge::ControllerGone() {
87 controller_ = nullptr;
91 void CardUnmaskPromptViewBridge::DisableAndWaitForVerification() {
92 [view_controller_ setProgressOverlayText:
93 l10n_util::GetStringUTF16(
94 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_IN_PROGRESS)
98 void CardUnmaskPromptViewBridge::GotVerificationResult(
99 const base::string16& error_message,
101 if (error_message.empty()) {
102 [view_controller_ setProgressOverlayText:
103 l10n_util::GetStringUTF16(
104 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_SUCCESS)
107 base::MessageLoop::current()->PostDelayedTask(
108 FROM_HERE, base::Bind(&CardUnmaskPromptViewBridge::PerformClose,
109 weak_ptr_factory_.GetWeakPtr()),
110 base::TimeDelta::FromSeconds(1));
112 [view_controller_ setProgressOverlayText:base::string16() showSpinner:NO];
115 // TODO(bondd): Views version never hides |errorLabel_|. When Views
116 // decides when to hide it then do the same thing here.
117 [view_controller_ setRetriableErrorMessage:error_message];
119 [view_controller_ setPermanentErrorMessage:error_message];
124 void CardUnmaskPromptViewBridge::OnConstrainedWindowClosed(
125 ConstrainedWindowMac* window) {
127 controller_->OnUnmaskDialogClosed();
128 base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
131 CardUnmaskPromptController* CardUnmaskPromptViewBridge::GetController() {
135 void CardUnmaskPromptViewBridge::PerformClose() {
136 constrained_window_->CloseWebContentsModalDialog();
141 #pragma mark CardUnmaskPromptViewCocoa
143 @implementation CardUnmaskPromptViewCocoa {
144 base::scoped_nsobject<NSBox> permanentErrorBox_;
145 base::scoped_nsobject<NSView> inputRowView_;
146 base::scoped_nsobject<NSView> progressOverlayView_;
147 base::scoped_nsobject<NSView> storageView_;
149 base::scoped_nsobject<NSTextField> titleLabel_;
150 base::scoped_nsobject<NSTextField> permanentErrorLabel_;
151 base::scoped_nsobject<NSTextField> instructionsLabel_;
152 base::scoped_nsobject<AutofillTextField> cvcInput_;
153 base::scoped_nsobject<AutofillPopUpButton> monthPopup_;
154 base::scoped_nsobject<AutofillPopUpButton> yearPopup_;
155 base::scoped_nsobject<NSButton> cancelButton_;
156 base::scoped_nsobject<NSButton> verifyButton_;
157 base::scoped_nsobject<NSButton> storageCheckbox_;
158 base::scoped_nsobject<AutofillTooltipController> storageTooltip_;
159 base::scoped_nsobject<NSTextField> errorLabel_;
160 base::scoped_nsobject<NSTextField> progressOverlayLabel_;
161 base::scoped_nsobject<SpinnerView> progressOverlaySpinner_;
163 int monthPopupDefaultIndex_;
164 int yearPopupDefaultIndex_;
167 autofill::CardUnmaskPromptViewBridge* bridge_;
170 + (AutofillPopUpButton*)buildDatePopupWithModel:(ui::ComboboxModel&)model {
171 AutofillPopUpButton* popup =
172 [[AutofillPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO];
174 for (int i = 0; i < model.GetItemCount(); ++i) {
175 [popup addItemWithTitle:base::SysUTF16ToNSString(model.GetItemAt(i))];
181 + (base::scoped_nsobject<NSBox>)createPlainBox {
182 base::scoped_nsobject<NSBox> box([[NSBox alloc] initWithFrame:NSZeroRect]);
183 [box setBoxType:NSBoxCustom];
184 [box setBorderType:NSNoBorder];
185 [box setTitlePosition:NSNoTitle];
189 // Set |view|'s frame to the minimum dimensions required to contain all of its
191 + (void)sizeToFitView:(NSView*)view {
192 NSRect frame = NSZeroRect;
193 for (NSView* child in [view subviews]) {
194 frame = NSUnionRect(frame, [child frame]);
196 [view setFrame:frame];
199 + (void)verticallyCenterSubviewsInView:(NSView*)view {
200 CGFloat height = NSHeight([view frame]);
201 for (NSView* child in [view subviews]) {
202 [child setFrameOrigin:NSMakePoint(
203 NSMinX([child frame]),
204 ceil((height - NSHeight([child frame])) * 0.5))];
208 - (id)initWithBridge:(autofill::CardUnmaskPromptViewBridge*)bridge {
211 if ((self = [super initWithNibName:nil bundle:nil]))
217 - (void)updateProgressOverlayOrigin {
218 // Center progressOverlayView_ horizontally in the dialog, and position it a
219 // fixed distance below instructionsLabel_.
220 CGFloat viewMinY = NSMinY([instructionsLabel_ frame]) -
221 kProgressToInstructionsGap -
222 NSHeight([progressOverlayView_ frame]);
223 [progressOverlayView_
224 setFrameOrigin:NSMakePoint(
225 NSMidX([[self view] frame]) -
226 NSWidth([progressOverlayView_ frame]) / 2.0,
230 - (void)setProgressOverlayText:(const base::string16&)text
231 showSpinner:(BOOL)showSpinner {
233 NSAttributedString* attributedString =
234 constrained_window::GetAttributedLabelString(
235 SysUTF16ToNSString(text), chrome_style::kTextFontStyle,
236 NSNaturalTextAlignment, NSLineBreakByWordWrapping);
237 [progressOverlayLabel_ setAttributedStringValue:attributedString];
238 [progressOverlayLabel_ sizeToFit];
239 CGFloat labelMinX = showSpinner
240 ? NSMaxX([progressOverlaySpinner_ frame]) +
241 kSpinnerToProgressTextGap
243 [progressOverlayLabel_ setFrameOrigin:NSMakePoint(labelMinX, 0)];
245 [CardUnmaskPromptViewCocoa sizeToFitView:progressOverlayView_];
246 [self updateProgressOverlayOrigin];
249 [progressOverlayView_ setHidden:text.empty()];
250 [progressOverlaySpinner_ setHidden:!showSpinner];
251 [inputRowView_ setHidden:!text.empty()];
252 [errorLabel_ setHidden:!text.empty()];
253 [storageCheckbox_ setHidden:!text.empty()];
254 [[storageTooltip_ view] setHidden:!text.empty()];
255 [self updateVerifyButtonEnabled];
258 - (void)setInputsEnabled:(BOOL)enabled {
259 [cvcInput_ setEnabled:enabled];
260 [monthPopup_ setEnabled:enabled];
261 [yearPopup_ setEnabled:enabled];
262 [storageCheckbox_ setEnabled:enabled];
265 - (void)setRetriableErrorMessage:(const base::string16&)text {
266 NSAttributedString* attributedString =
267 constrained_window::GetAttributedLabelString(
268 SysUTF16ToNSString(text), chrome_style::kTextFontStyle,
269 NSNaturalTextAlignment, NSLineBreakByWordWrapping);
270 [errorLabel_ setAttributedStringValue:attributedString];
272 // If there is more than one input showing, don't mark anything as
273 // invalid since we don't know the location of the problem.
274 if (!text.empty() && !bridge_->GetController()->ShouldRequestExpirationDate())
275 [cvcInput_ setValidityMessage:@"invalid"];
277 [self performLayoutAndDisplay:YES];
280 - (void)setPermanentErrorMessage:(const base::string16&)text {
282 if (!permanentErrorBox_) {
283 permanentErrorBox_ = [CardUnmaskPromptViewCocoa createPlainBox];
284 [permanentErrorBox_ setFillColor:gfx::SkColorToCalibratedNSColor(
285 autofill::kWarningColor)];
287 setContentViewMargins:NSMakeSize(kPermanentErrorHorizontalPadding,
288 kPermanentErrorVerticalPadding)];
290 permanentErrorLabel_.reset([constrained_window::CreateLabel() retain]);
291 [permanentErrorLabel_ setAutoresizingMask:NSViewWidthSizable];
292 [permanentErrorLabel_ setTextColor:gfx::SkColorToCalibratedNSColor(
293 kPermanentErrorTextColor)];
295 [permanentErrorBox_ addSubview:permanentErrorLabel_];
296 [[self view] addSubview:permanentErrorBox_];
299 NSAttributedString* attributedString =
300 constrained_window::GetAttributedLabelString(
301 SysUTF16ToNSString(text), chrome_style::kTextFontStyle,
302 NSNaturalTextAlignment, NSLineBreakByWordWrapping);
303 [permanentErrorLabel_ setAttributedStringValue:attributedString];
306 [permanentErrorBox_ setHidden:text.empty()];
307 [self setInputsEnabled:NO];
308 [self updateVerifyButtonEnabled];
309 [self setRetriableErrorMessage:base::string16()];
312 - (void)updateVerifyButtonEnabled {
313 BOOL enable = ![inputRowView_ isHidden] &&
314 ![[permanentErrorLabel_ stringValue] length] &&
315 bridge_->GetController()->InputCvcIsValid(
316 base::SysNSStringToUTF16([cvcInput_ stringValue])) &&
317 [self expirationDateIsValid];
319 [verifyButton_ setEnabled:enable];
322 - (void)onVerify:(id)sender {
323 bridge_->GetController()->OnUnmaskResponse(
324 base::SysNSStringToUTF16([cvcInput_ stringValue]),
325 base::SysNSStringToUTF16([monthPopup_ titleOfSelectedItem]),
326 base::SysNSStringToUTF16([yearPopup_ titleOfSelectedItem]),
327 [storageCheckbox_ state] == NSOnState);
330 - (void)onCancel:(id)sender {
331 bridge_->PerformClose();
334 - (BOOL)expirationDateIsValid {
335 if (!bridge_->GetController()->ShouldRequestExpirationDate())
338 return bridge_->GetController()->InputExpirationIsValid(
339 base::SysNSStringToUTF16([monthPopup_ titleOfSelectedItem]),
340 base::SysNSStringToUTF16([yearPopup_ titleOfSelectedItem]));
343 - (void)onExpirationDateChanged:(id)sender {
344 if ([self expirationDateIsValid]) {
345 if ([monthPopup_ invalid]) {
346 [monthPopup_ setValidityMessage:@""];
347 [yearPopup_ setValidityMessage:@""];
348 [self setRetriableErrorMessage:base::string16()];
350 } else if ([monthPopup_ indexOfSelectedItem] != monthPopupDefaultIndex_ &&
351 [yearPopup_ indexOfSelectedItem] != yearPopupDefaultIndex_) {
352 [monthPopup_ setValidityMessage:@"invalid"];
353 [yearPopup_ setValidityMessage:@"invalid"];
354 [self setRetriableErrorMessage:
355 l10n_util::GetStringUTF16(
356 IDS_AUTOFILL_CARD_UNMASK_INVALID_EXPIRATION_DATE)];
359 [self updateVerifyButtonEnabled];
362 // Called when text in CVC input field changes.
363 - (void)controlTextDidChange:(NSNotification*)notification {
364 if (bridge_->GetController()->InputCvcIsValid(
365 base::SysNSStringToUTF16([cvcInput_ stringValue])))
366 [cvcInput_ setValidityMessage:@""];
368 [self updateVerifyButtonEnabled];
371 - (base::scoped_nsobject<NSView>)createStorageViewWithController:
372 (autofill::CardUnmaskPromptController*)controller {
373 base::scoped_nsobject<NSView> view([[NSView alloc] initWithFrame:NSZeroRect]);
374 [view setAutoresizingMask:NSViewWidthSizable];
376 base::scoped_nsobject<NSBox> box = [CardUnmaskPromptViewCocoa createPlainBox];
377 [box setAutoresizingMask:NSViewWidthSizable];
378 [box setFillColor:gfx::SkColorToCalibratedNSColor(kShadingColor)];
379 [box setContentViewMargins:NSMakeSize(chrome_style::kHorizontalPadding,
380 chrome_style::kClientBottomPadding)];
381 [view addSubview:box];
383 // Add "Store card on this device" checkbox.
384 storageCheckbox_.reset([[NSButton alloc] initWithFrame:NSZeroRect]);
385 [storageCheckbox_ setButtonType:NSSwitchButton];
387 setTitle:base::SysUTF16ToNSString(l10n_util::GetStringUTF16(
388 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_CHECKBOX))];
390 setState:(controller->GetStoreLocallyStartState() ? NSOnState
392 [storageCheckbox_ sizeToFit];
393 [box addSubview:storageCheckbox_];
395 // Add "?" icon with tooltip.
396 storageTooltip_.reset([[AutofillTooltipController alloc]
397 initWithArrowLocation:info_bubble::kTopRight]);
398 [storageTooltip_ setImage:ui::ResourceBundle::GetSharedInstance()
399 .GetNativeImageNamed(IDR_AUTOFILL_TOOLTIP_ICON)
402 setMessage:base::SysUTF16ToNSString(l10n_util::GetStringUTF16(
403 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_TOOLTIP))];
404 [box addSubview:[storageTooltip_ view]];
405 [[storageTooltip_ view] setFrameOrigin:
406 NSMakePoint(NSMaxX([storageCheckbox_ frame]) + kButtonGap, 0)];
408 // Add horizontal separator.
409 base::scoped_nsobject<NSBox> separator =
410 [CardUnmaskPromptViewCocoa createPlainBox];
411 [separator setAutoresizingMask:NSViewWidthSizable];
412 [separator setFillColor:gfx::SkColorToCalibratedNSColor(kSubtleBorderColor)];
413 [view addSubview:separator];
416 [separator setFrame:NSMakeRect(0, NSMaxY([box frame]), NSWidth([box frame]),
418 [CardUnmaskPromptViewCocoa sizeToFitView:view];
422 // +---------------------------------------------------------------------------+
423 // | titleLabel_ (Single line.) |
424 // |---------------------------------------------------------------------------|
425 // | permanentErrorBox_ (Multiline, may be hidden.) |
426 // |---------------------------------------------------------------------------|
427 // | instructionsLabel_ (Multiline.) |
428 // |---------------------------------------------------------------------------|
429 // | monthPopup_ yearPopup_ cvcInput_ cvcImage |
430 // | (All enclosed in inputRowView_. Month and year may be hidden.) |
431 // |---------------------------------------------------------------------------|
432 // | errorLabel_ (Multiline. Always takes up space for one line even if |
433 // | empty. Hidden when progressOverlayView_ is displayed.) |
434 // |---------------------------------------------------------------------------|
435 // | [Cancel] [Verify] |
436 // |---------------------------------------------------------------------------|
437 // | separator (NSBox.) |
438 // | storageCheckbox_ storageTooltip_ (Both enclosed in another NSBox. |
439 // | Checkbox and tooltip may be hidden.) |
440 // | (Both NSBoxes are enclosed in storageView_. Will all be nil if |
441 // | !CanStoreLocally()). |
442 // +---------------------------------------------------------------------------+
444 // progressOverlayView_:
445 // (Displayed below instructionsLabel_, may be hidden.
446 // progressOverlaySpinner_ may be hidden while progressOverlayLabel_
448 // +---------------------------------------------------------------------------+
449 // | progressOverlaySpinner_ progressOverlayLabel_ |
450 // +---------------------------------------------------------------------------+
451 - (void)performLayoutAndDisplay:(BOOL)display {
452 // Calculate dialog content width.
453 CGFloat contentWidth =
454 std::max(NSWidth([titleLabel_ frame]), NSWidth([inputRowView_ frame]));
455 contentWidth = std::max(contentWidth,
456 NSWidth(NSUnionRect([storageCheckbox_ frame],
457 [[storageTooltip_ view] frame])));
458 contentWidth = std::max(contentWidth, kDialogContentMinWidth);
460 CGFloat contentMinX = chrome_style::kHorizontalPadding;
461 CGFloat contentMaxX = contentMinX + contentWidth;
462 CGFloat dialogWidth = contentMaxX + chrome_style::kHorizontalPadding;
465 storageView_ ? NSMaxY([storageView_ frame]) + chrome_style::kRowPadding
466 : chrome_style::kClientBottomPadding;
468 setFrameOrigin:NSMakePoint(contentMaxX - NSWidth([verifyButton_ frame]),
472 setFrameOrigin:NSMakePoint(NSMinX([verifyButton_ frame]) - kButtonGap -
473 NSWidth([cancelButton_ frame]),
474 NSMinY([verifyButton_ frame]))];
476 [errorLabel_ setFrame:NSMakeRect(contentMinX, NSMaxY([cancelButton_ frame]) +
477 kButtonsToRetriableErrorGap,
479 cocoa_l10n_util::WrapOrSizeToFit(errorLabel_);
481 [inputRowView_ setFrameOrigin:NSMakePoint(contentMinX,
482 NSMaxY([errorLabel_ frame]) +
483 kRetriableErrorToInputRowGap)];
486 setFrame:NSMakeRect(contentMinX, NSMaxY([inputRowView_ frame]) +
487 kInputRowToInstructionsGap,
489 cocoa_l10n_util::WrapOrSizeToFit(instructionsLabel_);
491 // Layout permanent error box.
493 if (permanentErrorBox_ && ![permanentErrorBox_ isHidden]) {
495 setFrame:NSMakeRect(0, NSMaxY([instructionsLabel_ frame]) +
496 kPermanentErrorExteriorPadding,
498 cocoa_l10n_util::WrapOrSizeToFit(permanentErrorLabel_);
499 [permanentErrorBox_ sizeToFit];
501 NSMaxY([permanentErrorBox_ frame]) + kPermanentErrorExteriorPadding;
503 titleMinY = NSMaxY([instructionsLabel_ frame]) + kInstructionsToTitleGap;
506 [titleLabel_ setFrameOrigin:NSMakePoint(contentMinX, titleMinY)];
510 setFrameSize:NSMakeSize(dialogWidth, NSMaxY([titleLabel_ frame]) +
511 chrome_style::kTitleTopPadding)];
513 [self updateProgressOverlayOrigin];
516 [[[self view] window] frameRectForContentRect:[[self view] frame]];
517 [[[self view] window] setFrame:frameRect display:display];
521 autofill::CardUnmaskPromptController* controller = bridge_->GetController();
524 base::scoped_nsobject<NSView> mainView(
525 [[NSView alloc] initWithFrame:NSZeroRect]);
527 inputRowView_.reset([[NSView alloc] initWithFrame:NSZeroRect]);
528 [mainView addSubview:inputRowView_];
530 if (controller->CanStoreLocally()) {
531 storageView_ = [self createStorageViewWithController:controller];
532 [mainView addSubview:storageView_];
535 // Add progress overlay.
536 progressOverlayView_.reset([[NSView alloc] initWithFrame:NSZeroRect]);
537 [progressOverlayView_ setHidden:YES];
538 [mainView addSubview:progressOverlayView_];
540 progressOverlayLabel_.reset([constrained_window::CreateLabel() retain]);
541 [progressOverlayLabel_
542 setTextColor:gfx::SkColorToCalibratedNSColor(kProgressTextColor)];
543 [progressOverlayView_ addSubview:progressOverlayLabel_];
545 progressOverlaySpinner_.reset([[SpinnerView alloc]
546 initWithFrame:NSMakeRect(0, 0, kSpinnerSize, kSpinnerSize)]);
547 [progressOverlayView_ addSubview:progressOverlaySpinner_];
550 titleLabel_.reset([constrained_window::CreateLabel() retain]);
551 NSAttributedString* titleString =
552 constrained_window::GetAttributedLabelString(
553 SysUTF16ToNSString(controller->GetWindowTitle()),
554 chrome_style::kTitleFontStyle, NSNaturalTextAlignment,
555 NSLineBreakByWordWrapping);
556 [titleLabel_ setAttributedStringValue:titleString];
557 [titleLabel_ sizeToFit];
558 [mainView addSubview:titleLabel_];
560 // Add instructions label.
561 instructionsLabel_.reset([constrained_window::CreateLabel() retain]);
562 NSAttributedString* instructionsString =
563 constrained_window::GetAttributedLabelString(
564 SysUTF16ToNSString(controller->GetInstructionsMessage()),
565 chrome_style::kTextFontStyle, NSNaturalTextAlignment,
566 NSLineBreakByWordWrapping);
567 [instructionsLabel_ setAttributedStringValue:instructionsString];
568 [mainView addSubview:instructionsLabel_];
570 // Add expiration date.
571 base::scoped_nsobject<NSView> expirationView;
572 if (controller->ShouldRequestExpirationDate()) {
573 expirationView.reset([[NSView alloc] initWithFrame:NSZeroRect]);
575 // Add expiration month.
576 autofill::MonthComboboxModel monthModel;
577 monthPopupDefaultIndex_ = monthModel.GetDefaultIndex();
579 [CardUnmaskPromptViewCocoa buildDatePopupWithModel:monthModel]);
580 [monthPopup_ setTarget:self];
581 [monthPopup_ setAction:@selector(onExpirationDateChanged:)];
582 [expirationView addSubview:monthPopup_];
584 // Add separator between month and year.
585 base::scoped_nsobject<NSTextField> separatorLabel(
586 [constrained_window::CreateLabel() retain]);
587 NSAttributedString* separatorString =
588 constrained_window::GetAttributedLabelString(
589 SysUTF16ToNSString(l10n_util::GetStringUTF16(
590 IDS_AUTOFILL_CARD_UNMASK_EXPIRATION_DATE_SEPARATOR)),
591 chrome_style::kTextFontStyle, NSNaturalTextAlignment,
592 NSLineBreakByWordWrapping);
593 [separatorLabel setAttributedStringValue:separatorString];
594 [separatorLabel sizeToFit];
595 [expirationView addSubview:separatorLabel];
597 // Add expiration year.
598 autofill::YearComboboxModel yearModel;
599 yearPopupDefaultIndex_ = yearModel.GetDefaultIndex();
601 [CardUnmaskPromptViewCocoa buildDatePopupWithModel:yearModel]);
602 [yearPopup_ setTarget:self];
603 [yearPopup_ setAction:@selector(onExpirationDateChanged:)];
604 [expirationView addSubview:yearPopup_];
606 // Layout month, separator, and year within expirationView.
607 [separatorLabel setFrameOrigin:NSMakePoint(NSMaxX([monthPopup_ frame]), 0)];
608 [yearPopup_ setFrameOrigin:NSMakePoint(NSMaxX([separatorLabel frame]), 0)];
609 NSRect expirationFrame =
610 NSUnionRect([monthPopup_ frame], [yearPopup_ frame]);
611 expirationFrame.size.width += kYearToCvcGap;
612 [expirationView setFrame:expirationFrame];
613 [CardUnmaskPromptViewCocoa verticallyCenterSubviewsInView:expirationView];
614 [inputRowView_ addSubview:expirationView];
617 // Add CVC text input.
618 cvcInput_.reset([[AutofillTextField alloc] initWithFrame:NSZeroRect]);
620 setPlaceholderString:l10n_util::GetNSString(
621 IDS_AUTOFILL_DIALOG_PLACEHOLDER_CVC)];
622 [[cvcInput_ cell] setScrollable:YES];
623 [cvcInput_ setDelegate:self];
624 [cvcInput_ sizeToFit];
625 [cvcInput_ setFrame:NSMakeRect(NSMaxX([expirationView frame]), 0,
626 kCvcInputWidth, NSHeight([cvcInput_ frame]))];
627 [inputRowView_ addSubview:cvcInput_];
630 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
632 rb.GetNativeImageNamed(controller->GetCvcImageRid()).ToNSImage();
633 base::scoped_nsobject<NSImageView> cvcImageView(
634 [[NSImageView alloc] initWithFrame:NSZeroRect]);
635 [cvcImageView setImage:cvcImage];
636 [cvcImageView setFrameSize:[cvcImage size]];
638 setFrameOrigin:NSMakePoint(
639 NSMaxX([cvcInput_ frame]) + kCvcInputToImageGap, 0)];
640 [inputRowView_ addSubview:cvcImageView];
642 // Add error message label.
643 errorLabel_.reset([constrained_window::CreateLabel() retain]);
645 setTextColor:gfx::SkColorToCalibratedNSColor(autofill::kWarningColor)];
646 [mainView addSubview:errorLabel_];
648 // Add cancel button.
650 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
651 [cancelButton_ setTitle:l10n_util::GetNSStringWithFixup(IDS_CANCEL)];
652 [cancelButton_ setKeyEquivalent:kKeyEquivalentEscape];
653 [cancelButton_ setTarget:self];
654 [cancelButton_ setAction:@selector(onCancel:)];
655 [cancelButton_ sizeToFit];
656 [mainView addSubview:cancelButton_];
658 // Add verify button.
660 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
661 [verifyButton_ setTitle:l10n_util::GetNSStringWithFixup(
662 IDS_AUTOFILL_CARD_UNMASK_CONFIRM_BUTTON)];
663 [verifyButton_ setKeyEquivalent:kKeyEquivalentReturn];
664 [verifyButton_ setTarget:self];
665 [verifyButton_ setAction:@selector(onVerify:)];
666 [verifyButton_ sizeToFit];
667 [self updateVerifyButtonEnabled];
668 [mainView addSubview:verifyButton_];
670 // Layout inputRowView_.
671 [CardUnmaskPromptViewCocoa sizeToFitView:inputRowView_];
672 [CardUnmaskPromptViewCocoa verticallyCenterSubviewsInView:inputRowView_];
674 [self setView:mainView];
675 [self performLayoutAndDisplay:NO];