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/themes/theme_properties.h"
9 #include "chrome/browser/ui/autofill/autofill_dialog_models.h"
10 #include "chrome/browser/ui/autofill/autofill_dialog_types.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 "chrome/browser/ui/cocoa/themed_window.h"
24 #include "components/autofill/core/browser/ui/card_unmask_prompt_controller.h"
25 #include "content/public/browser/web_contents.h"
26 #include "grit/generated_resources.h"
27 #include "grit/theme_resources.h"
28 #include "skia/ext/skia_utils_mac.h"
29 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
30 #include "ui/base/cocoa/window_size_constants.h"
31 #include "ui/base/l10n/l10n_util.h"
32 #include "ui/base/theme_provider.h"
36 const CGFloat kButtonGap = 6.0f;
37 const CGFloat kButtonsToRetriableErrorGap = 12.0f;
38 const CGFloat kCvcInputWidth = 64.0f;
39 const CGFloat kCvcInputToImageGap = 8.0f;
40 const CGFloat kDialogContentMinWidth = 210.0f;
41 const CGFloat kInputRowToInstructionsGap = 16.0f;
42 const CGFloat kInstructionsToTitleGap = 8.0f;
43 const CGFloat kPermanentErrorExteriorPadding = 12.0f;
44 const CGFloat kPermanentErrorHorizontalPadding = 16.0f;
45 const CGFloat kPermanentErrorVerticalPadding = 12.0f;
46 const CGFloat kProgressToInstructionsGap = 24.0f;
47 const CGFloat kRetriableErrorToInputRowGap = 4.0f;
48 const CGFloat kSeparatorHeight = 1.0f;
49 const CGFloat kSpinnerSize = 16.0f;
50 const CGFloat kSpinnerToProgressTextGap = 8.0f;
51 const CGFloat kYearToCvcGap = 12.0f;
53 const SkColor kPermanentErrorTextColor = SK_ColorWHITE;
54 // TODO(bondd): Unify colors with Views version and AutofillMessageView.
55 const SkColor kShadingColor = SkColorSetRGB(0xf2, 0xf2, 0xf2);
56 const SkColor kSubtleBorderColor = SkColorSetRGB(0xdf, 0xdf, 0xdf);
62 CardUnmaskPromptView* CreateCardUnmaskPromptView(
63 CardUnmaskPromptController* controller,
64 content::WebContents* web_contents) {
65 return new CardUnmaskPromptViewBridge(controller, web_contents);
68 #pragma mark CardUnmaskPromptViewBridge
70 CardUnmaskPromptViewBridge::CardUnmaskPromptViewBridge(
71 CardUnmaskPromptController* controller,
72 content::WebContents* web_contents)
73 : controller_(controller),
74 web_contents_(web_contents),
75 weak_ptr_factory_(this) {
76 view_controller_.reset(
77 [[CardUnmaskPromptViewCocoa alloc] initWithBridge:this]);
80 CardUnmaskPromptViewBridge::~CardUnmaskPromptViewBridge() {
83 void CardUnmaskPromptViewBridge::Show() {
84 // Setup the constrained window that will show the view.
85 base::scoped_nsobject<NSWindow> window([[ConstrainedWindowCustomWindow alloc]
86 initWithContentRect:[[view_controller_ view] bounds]]);
87 [window setContentView:[view_controller_ view]];
88 base::scoped_nsobject<CustomConstrainedWindowSheet> sheet(
89 [[CustomConstrainedWindowSheet alloc] initWithCustomWindow:window]);
90 constrained_window_.reset(
91 new ConstrainedWindowMac(this, web_contents_, sheet));
94 void CardUnmaskPromptViewBridge::ControllerGone() {
95 controller_ = nullptr;
99 void CardUnmaskPromptViewBridge::DisableAndWaitForVerification() {
100 [view_controller_ setProgressOverlayText:
101 l10n_util::GetStringUTF16(
102 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_IN_PROGRESS)
106 void CardUnmaskPromptViewBridge::GotVerificationResult(
107 const base::string16& error_message,
109 if (error_message.empty()) {
110 [view_controller_ setProgressOverlayText:
111 l10n_util::GetStringUTF16(
112 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_SUCCESS)
115 base::MessageLoop::current()->PostDelayedTask(
116 FROM_HERE, base::Bind(&CardUnmaskPromptViewBridge::PerformClose,
117 weak_ptr_factory_.GetWeakPtr()),
118 base::TimeDelta::FromSeconds(1));
120 [view_controller_ setProgressOverlayText:base::string16() showSpinner:NO];
123 // TODO(bondd): Views version never hides |errorLabel_|. When Views
124 // decides when to hide it then do the same thing here.
125 [view_controller_ setRetriableErrorMessage:error_message];
127 [view_controller_ setPermanentErrorMessage:error_message];
132 void CardUnmaskPromptViewBridge::OnConstrainedWindowClosed(
133 ConstrainedWindowMac* window) {
135 controller_->OnUnmaskDialogClosed();
136 base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
139 CardUnmaskPromptController* CardUnmaskPromptViewBridge::GetController() {
143 content::WebContents* CardUnmaskPromptViewBridge::GetWebContents() {
144 return web_contents_;
147 void CardUnmaskPromptViewBridge::PerformClose() {
148 constrained_window_->CloseWebContentsModalDialog();
153 #pragma mark CardUnmaskPromptViewCocoa
155 @implementation CardUnmaskPromptViewCocoa {
156 base::scoped_nsobject<NSBox> permanentErrorBox_;
157 base::scoped_nsobject<NSView> expirationView_;
158 base::scoped_nsobject<NSView> inputRowView_;
159 base::scoped_nsobject<NSView> progressOverlayView_;
160 base::scoped_nsobject<NSView> storageView_;
162 base::scoped_nsobject<NSTextField> titleLabel_;
163 base::scoped_nsobject<NSTextField> permanentErrorLabel_;
164 base::scoped_nsobject<NSTextField> instructionsLabel_;
165 base::scoped_nsobject<AutofillTextField> cvcInput_;
166 base::scoped_nsobject<AutofillPopUpButton> monthPopup_;
167 base::scoped_nsobject<AutofillPopUpButton> yearPopup_;
168 base::scoped_nsobject<NSImageView> cvcImageView_;
169 base::scoped_nsobject<NSButton> newCardButton_;
170 base::scoped_nsobject<NSButton> cancelButton_;
171 base::scoped_nsobject<NSButton> verifyButton_;
172 base::scoped_nsobject<NSButton> storageCheckbox_;
173 base::scoped_nsobject<AutofillTooltipController> storageTooltip_;
174 base::scoped_nsobject<NSTextField> errorLabel_;
175 base::scoped_nsobject<NSTextField> progressOverlayLabel_;
176 base::scoped_nsobject<SpinnerView> progressOverlaySpinner_;
178 int monthPopupDefaultIndex_;
179 int yearPopupDefaultIndex_;
182 autofill::CardUnmaskPromptViewBridge* bridge_;
185 + (AutofillPopUpButton*)buildDatePopupWithModel:(ui::ComboboxModel&)model {
186 AutofillPopUpButton* popup =
187 [[AutofillPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO];
189 for (int i = 0; i < model.GetItemCount(); ++i) {
190 [popup addItemWithTitle:base::SysUTF16ToNSString(model.GetItemAt(i))];
196 + (base::scoped_nsobject<NSBox>)createPlainBox {
197 base::scoped_nsobject<NSBox> box([[NSBox alloc] initWithFrame:NSZeroRect]);
198 [box setBoxType:NSBoxCustom];
199 [box setBorderType:NSNoBorder];
200 [box setTitlePosition:NSNoTitle];
204 // Set |view|'s frame to the minimum dimensions required to contain all of its
206 + (void)sizeToFitView:(NSView*)view {
207 NSRect frame = NSZeroRect;
208 for (NSView* child in [view subviews]) {
209 frame = NSUnionRect(frame, [child frame]);
211 [view setFrame:frame];
214 + (void)verticallyCenterSubviewsInView:(NSView*)view {
215 CGFloat height = NSHeight([view frame]);
216 for (NSView* child in [view subviews]) {
217 [child setFrameOrigin:NSMakePoint(
218 NSMinX([child frame]),
219 ceil((height - NSHeight([child frame])) * 0.5))];
223 - (id)initWithBridge:(autofill::CardUnmaskPromptViewBridge*)bridge {
226 if ((self = [super initWithNibName:nil bundle:nil]))
232 - (void)updateProgressOverlayOrigin {
233 // Center |progressOverlayView_| horizontally in the dialog, and position it
234 // a fixed distance below |instructionsLabel_|.
235 CGFloat viewMinY = NSMinY([instructionsLabel_ frame]) -
236 kProgressToInstructionsGap -
237 NSHeight([progressOverlayView_ frame]);
238 [progressOverlayView_
239 setFrameOrigin:NSMakePoint(
240 NSMidX([[self view] frame]) -
241 NSWidth([progressOverlayView_ frame]) / 2.0,
245 - (void)setProgressOverlayText:(const base::string16&)text
246 showSpinner:(BOOL)showSpinner {
248 NSAttributedString* attributedString =
249 constrained_window::GetAttributedLabelString(
250 SysUTF16ToNSString(text), chrome_style::kTextFontStyle,
251 NSNaturalTextAlignment, NSLineBreakByWordWrapping);
252 [progressOverlayLabel_ setAttributedStringValue:attributedString];
253 [progressOverlayLabel_ sizeToFit];
254 CGFloat labelMinX = showSpinner
255 ? NSMaxX([progressOverlaySpinner_ frame]) +
256 kSpinnerToProgressTextGap
258 [progressOverlayLabel_ setFrameOrigin:NSMakePoint(labelMinX, 0)];
260 [CardUnmaskPromptViewCocoa sizeToFitView:progressOverlayView_];
261 [self updateProgressOverlayOrigin];
264 [progressOverlayView_ setHidden:text.empty()];
265 [progressOverlaySpinner_ setHidden:!showSpinner];
266 [inputRowView_ setHidden:!text.empty()];
267 [errorLabel_ setHidden:!text.empty()];
268 [storageCheckbox_ setHidden:!text.empty()];
269 [[storageTooltip_ view] setHidden:!text.empty()];
270 [self updateVerifyButtonEnabled];
273 - (void)setInputsEnabled:(BOOL)enabled {
274 [cvcInput_ setEnabled:enabled];
275 [monthPopup_ setEnabled:enabled];
276 [yearPopup_ setEnabled:enabled];
277 [newCardButton_ setEnabled:enabled];
278 [storageCheckbox_ setEnabled:enabled];
281 - (void)setRetriableErrorMessage:(const base::string16&)text {
282 NSAttributedString* attributedString =
283 constrained_window::GetAttributedLabelString(
284 SysUTF16ToNSString(text), chrome_style::kTextFontStyle,
285 NSNaturalTextAlignment, NSLineBreakByWordWrapping);
286 [errorLabel_ setAttributedStringValue:attributedString];
288 // If there is more than one input showing, don't mark anything as invalid
289 // since we don't know the location of the problem.
291 !bridge_->GetController()->ShouldRequestExpirationDate()) {
292 [cvcInput_ setValidityMessage:@"invalid"];
294 // Show "New card?" button, which when clicked will cause this dialog to
295 // ask for expiration date.
296 [self createNewCardButton];
297 [inputRowView_ addSubview:newCardButton_];
300 [self performLayoutAndDisplay:YES];
303 - (void)setPermanentErrorMessage:(const base::string16&)text {
305 if (!permanentErrorBox_) {
306 permanentErrorBox_ = [CardUnmaskPromptViewCocoa createPlainBox];
307 [permanentErrorBox_ setFillColor:gfx::SkColorToCalibratedNSColor(
308 autofill::kWarningColor)];
310 setContentViewMargins:NSMakeSize(kPermanentErrorHorizontalPadding,
311 kPermanentErrorVerticalPadding)];
313 permanentErrorLabel_.reset([constrained_window::CreateLabel() retain]);
314 [permanentErrorLabel_ setAutoresizingMask:NSViewWidthSizable];
315 [permanentErrorLabel_ setTextColor:gfx::SkColorToCalibratedNSColor(
316 kPermanentErrorTextColor)];
318 [permanentErrorBox_ addSubview:permanentErrorLabel_];
319 [[self view] addSubview:permanentErrorBox_];
322 NSAttributedString* attributedString =
323 constrained_window::GetAttributedLabelString(
324 SysUTF16ToNSString(text), chrome_style::kTextFontStyle,
325 NSNaturalTextAlignment, NSLineBreakByWordWrapping);
326 [permanentErrorLabel_ setAttributedStringValue:attributedString];
329 [permanentErrorBox_ setHidden:text.empty()];
330 [self setInputsEnabled:NO];
331 [self updateVerifyButtonEnabled];
332 [self setRetriableErrorMessage:base::string16()];
335 - (void)updateVerifyButtonEnabled {
336 BOOL enable = ![inputRowView_ isHidden] &&
337 ![[permanentErrorLabel_ stringValue] length] &&
338 bridge_->GetController()->InputCvcIsValid(
339 base::SysNSStringToUTF16([cvcInput_ stringValue])) &&
340 [self expirationDateIsValid];
342 [verifyButton_ setEnabled:enable];
345 - (void)onVerify:(id)sender {
346 bridge_->GetController()->OnUnmaskResponse(
347 base::SysNSStringToUTF16([cvcInput_ stringValue]),
348 base::SysNSStringToUTF16([monthPopup_ titleOfSelectedItem]),
349 base::SysNSStringToUTF16([yearPopup_ titleOfSelectedItem]),
350 [storageCheckbox_ state] == NSOnState);
353 - (void)onCancel:(id)sender {
354 bridge_->PerformClose();
357 - (void)onNewCard:(id)sender {
358 autofill::CardUnmaskPromptController* controller = bridge_->GetController();
359 controller->NewCardLinkClicked();
361 // |newCardButton_| will never be shown again for this dialog.
362 [newCardButton_ removeFromSuperview];
363 newCardButton_.reset();
365 [self createExpirationView];
366 [inputRowView_ addSubview:expirationView_];
367 [cvcInput_ setStringValue:@""];
368 [cvcInput_ setValidityMessage:@""];
369 [self setRetriableErrorMessage:base::string16()];
370 [self updateInstructionsText];
371 [self updateVerifyButtonEnabled];
372 [self performLayoutAndDisplay:YES];
375 - (BOOL)expirationDateIsValid {
376 if (!bridge_->GetController()->ShouldRequestExpirationDate())
379 return bridge_->GetController()->InputExpirationIsValid(
380 base::SysNSStringToUTF16([monthPopup_ titleOfSelectedItem]),
381 base::SysNSStringToUTF16([yearPopup_ titleOfSelectedItem]));
384 - (void)onExpirationDateChanged:(id)sender {
385 if ([self expirationDateIsValid]) {
386 if ([monthPopup_ invalid]) {
387 [monthPopup_ setValidityMessage:@""];
388 [yearPopup_ setValidityMessage:@""];
389 [self setRetriableErrorMessage:base::string16()];
391 } else if ([monthPopup_ indexOfSelectedItem] != monthPopupDefaultIndex_ &&
392 [yearPopup_ indexOfSelectedItem] != yearPopupDefaultIndex_) {
393 [monthPopup_ setValidityMessage:@"invalid"];
394 [yearPopup_ setValidityMessage:@"invalid"];
395 [self setRetriableErrorMessage:
396 l10n_util::GetStringUTF16(
397 IDS_AUTOFILL_CARD_UNMASK_INVALID_EXPIRATION_DATE)];
400 [self updateVerifyButtonEnabled];
403 // Called when text in CVC input field changes.
404 - (void)controlTextDidChange:(NSNotification*)notification {
405 if (bridge_->GetController()->InputCvcIsValid(
406 base::SysNSStringToUTF16([cvcInput_ stringValue])))
407 [cvcInput_ setValidityMessage:@""];
409 [self updateVerifyButtonEnabled];
412 - (void)updateInstructionsText {
413 NSAttributedString* instructionsString =
414 constrained_window::GetAttributedLabelString(
416 bridge_->GetController()->GetInstructionsMessage()),
417 chrome_style::kTextFontStyle, NSNaturalTextAlignment,
418 NSLineBreakByWordWrapping);
419 [instructionsLabel_ setAttributedStringValue:instructionsString];
422 - (void)createNewCardButton {
423 DCHECK(!newCardButton_);
424 newCardButton_.reset([[HyperlinkButtonCell
425 buttonWithString:l10n_util::GetNSString(
426 IDS_AUTOFILL_CARD_UNMASK_NEW_CARD_LINK)] retain]);
427 [[newCardButton_ cell] setShouldUnderline:NO];
428 [newCardButton_ setTarget:self];
429 [newCardButton_ setAction:@selector(onNewCard:)];
430 [newCardButton_ sizeToFit];
433 - (void)createExpirationView {
434 DCHECK(!expirationView_);
435 expirationView_.reset([[NSView alloc] initWithFrame:NSZeroRect]);
437 // Add expiration month.
438 autofill::MonthComboboxModel monthModel;
439 monthPopupDefaultIndex_ = monthModel.GetDefaultIndex();
441 [CardUnmaskPromptViewCocoa buildDatePopupWithModel:monthModel]);
442 [monthPopup_ setTarget:self];
443 [monthPopup_ setAction:@selector(onExpirationDateChanged:)];
444 [expirationView_ addSubview:monthPopup_];
446 // Add separator between month and year.
447 base::scoped_nsobject<NSTextField> separatorLabel(
448 [constrained_window::CreateLabel() retain]);
449 NSAttributedString* separatorString =
450 constrained_window::GetAttributedLabelString(
451 SysUTF16ToNSString(l10n_util::GetStringUTF16(
452 IDS_AUTOFILL_CARD_UNMASK_EXPIRATION_DATE_SEPARATOR)),
453 chrome_style::kTextFontStyle, NSNaturalTextAlignment,
454 NSLineBreakByWordWrapping);
455 [separatorLabel setAttributedStringValue:separatorString];
456 [separatorLabel sizeToFit];
457 [expirationView_ addSubview:separatorLabel];
459 // Add expiration year.
460 autofill::YearComboboxModel yearModel;
461 yearPopupDefaultIndex_ = yearModel.GetDefaultIndex();
463 [CardUnmaskPromptViewCocoa buildDatePopupWithModel:yearModel]);
464 [yearPopup_ setTarget:self];
465 [yearPopup_ setAction:@selector(onExpirationDateChanged:)];
466 [expirationView_ addSubview:yearPopup_];
468 // Lay out month, separator, and year within |expirationView_|.
469 [separatorLabel setFrameOrigin:NSMakePoint(NSMaxX([monthPopup_ frame]), 0)];
470 [yearPopup_ setFrameOrigin:NSMakePoint(NSMaxX([separatorLabel frame]), 0)];
471 NSRect expirationFrame = NSUnionRect([monthPopup_ frame], [yearPopup_ frame]);
472 expirationFrame.size.width += kYearToCvcGap;
473 [expirationView_ setFrame:expirationFrame];
474 [CardUnmaskPromptViewCocoa verticallyCenterSubviewsInView:expirationView_];
477 - (void)createStorageViewWithController:
478 (autofill::CardUnmaskPromptController*)controller {
479 DCHECK(!storageView_);
480 storageView_.reset([[NSView alloc] initWithFrame:NSZeroRect]);
481 [storageView_ setAutoresizingMask:NSViewWidthSizable];
483 base::scoped_nsobject<NSBox> box = [CardUnmaskPromptViewCocoa createPlainBox];
484 [box setAutoresizingMask:NSViewWidthSizable];
485 [box setFillColor:gfx::SkColorToCalibratedNSColor(kShadingColor)];
486 [box setContentViewMargins:NSMakeSize(chrome_style::kHorizontalPadding,
487 chrome_style::kClientBottomPadding)];
488 [storageView_ addSubview:box];
490 // Add "Store card on this device" checkbox.
491 storageCheckbox_.reset([[NSButton alloc] initWithFrame:NSZeroRect]);
492 [storageCheckbox_ setButtonType:NSSwitchButton];
494 setTitle:base::SysUTF16ToNSString(l10n_util::GetStringUTF16(
495 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_CHECKBOX))];
497 setState:(controller->GetStoreLocallyStartState() ? NSOnState
499 [storageCheckbox_ sizeToFit];
500 [box addSubview:storageCheckbox_];
502 // Add "?" icon with tooltip.
503 storageTooltip_.reset([[AutofillTooltipController alloc]
504 initWithArrowLocation:info_bubble::kTopRight]);
505 [storageTooltip_ setImage:ui::ResourceBundle::GetSharedInstance()
506 .GetNativeImageNamed(IDR_AUTOFILL_TOOLTIP_ICON)
509 setMessage:base::SysUTF16ToNSString(l10n_util::GetStringUTF16(
510 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_TOOLTIP))];
511 [box addSubview:[storageTooltip_ view]];
512 [[storageTooltip_ view] setFrameOrigin:
513 NSMakePoint(NSMaxX([storageCheckbox_ frame]) + kButtonGap, 0)];
515 // Add horizontal separator.
516 base::scoped_nsobject<NSBox> separator =
517 [CardUnmaskPromptViewCocoa createPlainBox];
518 [separator setAutoresizingMask:NSViewWidthSizable];
519 [separator setFillColor:gfx::SkColorToCalibratedNSColor(kSubtleBorderColor)];
520 [storageView_ addSubview:separator];
523 [separator setFrame:NSMakeRect(0, NSMaxY([box frame]), NSWidth([box frame]),
525 [CardUnmaskPromptViewCocoa sizeToFitView:storageView_];
528 // +---------------------------------------------------------------------------+
529 // | titleLabel_ (Single line.) |
530 // |---------------------------------------------------------------------------|
531 // | permanentErrorBox_ (Multiline, may be hidden.) |
532 // |---------------------------------------------------------------------------|
533 // | instructionsLabel_ (Multiline.) |
534 // |---------------------------------------------------------------------------|
535 // | monthPopup_ yearPopup_ cvcInput_ cvcImageView_ newCardButton_ |
536 // | (All enclosed in inputRowView_. month, year, and newCard may be |
538 // |---------------------------------------------------------------------------|
539 // | errorLabel_ (Multiline. Always takes up space for one line even if empty. |
540 // | Hidden when progressOverlayView_ is displayed.) |
541 // |---------------------------------------------------------------------------|
542 // | [Cancel] [Verify] |
543 // |---------------------------------------------------------------------------|
544 // | separator (NSBox.) |
545 // | storageCheckbox_ storageTooltip_ (Both enclosed in another NSBox. |
546 // | Checkbox and tooltip may be hidden.) |
547 // | (Both NSBoxes are enclosed in storageView_. Will all be nil if |
548 // | !CanStoreLocally()). |
549 // +---------------------------------------------------------------------------+
551 // progressOverlayView_:
552 // (Displayed below instructionsLabel_, may be hidden.
553 // progressOverlaySpinner_ may be hidden while progressOverlayLabel_
555 // +---------------------------------------------------------------------------+
556 // | progressOverlaySpinner_ progressOverlayLabel_ |
557 // +---------------------------------------------------------------------------+
558 - (void)performLayoutAndDisplay:(BOOL)display {
559 // Lay out |inputRowView_| contents.
560 // |expirationView_| may be nil, which will result in |cvcInput_| getting an
562 [cvcInput_ setFrame:NSMakeRect(NSMaxX([expirationView_ frame]), 0,
563 kCvcInputWidth, NSHeight([cvcInput_ frame]))];
565 setFrameOrigin:NSMakePoint(
566 NSMaxX([cvcInput_ frame]) + kCvcInputToImageGap, 0)];
568 setFrameOrigin:NSMakePoint(
569 NSMaxX([cvcImageView_ frame]) + kButtonGap, 0)];
570 [CardUnmaskPromptViewCocoa sizeToFitView:inputRowView_];
571 [CardUnmaskPromptViewCocoa verticallyCenterSubviewsInView:inputRowView_];
573 // Calculate dialog content width.
574 CGFloat contentWidth =
575 std::max(NSWidth([titleLabel_ frame]), NSWidth([inputRowView_ frame]));
576 contentWidth = std::max(contentWidth,
577 NSWidth(NSUnionRect([storageCheckbox_ frame],
578 [[storageTooltip_ view] frame])));
579 contentWidth = std::max(contentWidth, kDialogContentMinWidth);
581 CGFloat contentMinX = chrome_style::kHorizontalPadding;
582 CGFloat contentMaxX = contentMinX + contentWidth;
583 CGFloat dialogWidth = contentMaxX + chrome_style::kHorizontalPadding;
586 storageView_ ? NSMaxY([storageView_ frame]) + chrome_style::kRowPadding
587 : chrome_style::kClientBottomPadding;
589 setFrameOrigin:NSMakePoint(contentMaxX - NSWidth([verifyButton_ frame]),
593 setFrameOrigin:NSMakePoint(NSMinX([verifyButton_ frame]) - kButtonGap -
594 NSWidth([cancelButton_ frame]),
595 NSMinY([verifyButton_ frame]))];
597 [errorLabel_ setFrame:NSMakeRect(contentMinX, NSMaxY([cancelButton_ frame]) +
598 kButtonsToRetriableErrorGap,
600 cocoa_l10n_util::WrapOrSizeToFit(errorLabel_);
602 [inputRowView_ setFrameOrigin:NSMakePoint(contentMinX,
603 NSMaxY([errorLabel_ frame]) +
604 kRetriableErrorToInputRowGap)];
607 setFrame:NSMakeRect(contentMinX, NSMaxY([inputRowView_ frame]) +
608 kInputRowToInstructionsGap,
610 cocoa_l10n_util::WrapOrSizeToFit(instructionsLabel_);
612 // Lay out permanent error box.
614 if (permanentErrorBox_ && ![permanentErrorBox_ isHidden]) {
616 setFrame:NSMakeRect(0, NSMaxY([instructionsLabel_ frame]) +
617 kPermanentErrorExteriorPadding,
619 cocoa_l10n_util::WrapOrSizeToFit(permanentErrorLabel_);
620 [permanentErrorBox_ sizeToFit];
622 NSMaxY([permanentErrorBox_ frame]) + kPermanentErrorExteriorPadding;
624 titleMinY = NSMaxY([instructionsLabel_ frame]) + kInstructionsToTitleGap;
627 [titleLabel_ setFrameOrigin:NSMakePoint(contentMinX, titleMinY)];
631 setFrameSize:NSMakeSize(dialogWidth, NSMaxY([titleLabel_ frame]) +
632 chrome_style::kTitleTopPadding)];
634 [self updateProgressOverlayOrigin];
637 [[[self view] window] frameRectForContentRect:[[self view] frame]];
638 [[[self view] window] setFrame:frameRect display:display];
642 autofill::CardUnmaskPromptController* controller = bridge_->GetController();
645 base::scoped_nsobject<NSView> mainView(
646 [[NSView alloc] initWithFrame:NSZeroRect]);
648 inputRowView_.reset([[NSView alloc] initWithFrame:NSZeroRect]);
649 [mainView addSubview:inputRowView_];
651 if (controller->CanStoreLocally()) {
652 [self createStorageViewWithController:controller];
653 [mainView addSubview:storageView_];
656 // Add progress overlay.
657 progressOverlayView_.reset([[NSView alloc] initWithFrame:NSZeroRect]);
658 [progressOverlayView_ setHidden:YES];
659 [mainView addSubview:progressOverlayView_];
661 progressOverlayLabel_.reset([constrained_window::CreateLabel() retain]);
662 NSView* webContentView = bridge_->GetWebContents()->GetNativeView();
663 ui::ThemeProvider* themeProvider = [[webContentView window] themeProvider];
664 DCHECK(themeProvider);
665 NSColor* throbberBlueColor = themeProvider->GetNSColor(
666 ThemeProperties::COLOR_THROBBER_SPINNING);
667 [progressOverlayLabel_ setTextColor:throbberBlueColor];
668 [progressOverlayView_ addSubview:progressOverlayLabel_];
670 progressOverlaySpinner_.reset([[SpinnerView alloc]
671 initWithFrame:NSMakeRect(0, 0, kSpinnerSize, kSpinnerSize)]);
672 [progressOverlayView_ addSubview:progressOverlaySpinner_];
675 titleLabel_.reset([constrained_window::CreateLabel() retain]);
676 NSAttributedString* titleString =
677 constrained_window::GetAttributedLabelString(
678 SysUTF16ToNSString(controller->GetWindowTitle()),
679 chrome_style::kTitleFontStyle, NSNaturalTextAlignment,
680 NSLineBreakByWordWrapping);
681 [titleLabel_ setAttributedStringValue:titleString];
682 [titleLabel_ sizeToFit];
683 [mainView addSubview:titleLabel_];
685 // Add instructions label.
686 instructionsLabel_.reset([constrained_window::CreateLabel() retain]);
687 [self updateInstructionsText];
688 [mainView addSubview:instructionsLabel_];
690 // Add expiration date.
691 if (controller->ShouldRequestExpirationDate()) {
692 [self createExpirationView];
693 [inputRowView_ addSubview:expirationView_];
696 // Add CVC text input.
697 cvcInput_.reset([[AutofillTextField alloc] initWithFrame:NSZeroRect]);
699 setPlaceholderString:l10n_util::GetNSString(
700 IDS_AUTOFILL_DIALOG_PLACEHOLDER_CVC)];
701 [[cvcInput_ cell] setScrollable:YES];
702 [cvcInput_ setDelegate:self];
703 [cvcInput_ sizeToFit];
704 [inputRowView_ addSubview:cvcInput_];
707 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
709 rb.GetNativeImageNamed(controller->GetCvcImageRid()).ToNSImage();
710 cvcImageView_.reset([[NSImageView alloc] initWithFrame:NSZeroRect]);
711 [cvcImageView_ setImage:cvcImage];
712 [cvcImageView_ setFrameSize:[cvcImage size]];
713 [inputRowView_ addSubview:cvcImageView_];
715 // Add error message label.
716 errorLabel_.reset([constrained_window::CreateLabel() retain]);
718 setTextColor:gfx::SkColorToCalibratedNSColor(autofill::kWarningColor)];
719 [mainView addSubview:errorLabel_];
721 // Add cancel button.
723 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
724 [cancelButton_ setTitle:l10n_util::GetNSStringWithFixup(IDS_CANCEL)];
725 [cancelButton_ setKeyEquivalent:kKeyEquivalentEscape];
726 [cancelButton_ setTarget:self];
727 [cancelButton_ setAction:@selector(onCancel:)];
728 [cancelButton_ sizeToFit];
729 [mainView addSubview:cancelButton_];
731 // Add verify button.
733 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
734 [verifyButton_ setTitle:l10n_util::GetNSStringWithFixup(
735 IDS_AUTOFILL_CARD_UNMASK_CONFIRM_BUTTON)];
736 [verifyButton_ setKeyEquivalent:kKeyEquivalentReturn];
737 [verifyButton_ setTarget:self];
738 [verifyButton_ setAction:@selector(onVerify:)];
739 [verifyButton_ sizeToFit];
740 [self updateVerifyButtonEnabled];
741 [mainView addSubview:verifyButton_];
743 [self setView:mainView];
744 [self performLayoutAndDisplay:NO];