1 // Copyright (c) 2013 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 #import "chrome/browser/ui/cocoa/autofill/autofill_section_container.h"
9 #include "base/mac/foundation_util.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h"
13 #import "chrome/browser/ui/cocoa/autofill/autofill_pop_up_button.h"
14 #import "chrome/browser/ui/cocoa/autofill/autofill_section_view.h"
15 #import "chrome/browser/ui/cocoa/autofill/autofill_suggestion_container.h"
16 #import "chrome/browser/ui/cocoa/autofill/autofill_textfield.h"
17 #import "chrome/browser/ui/cocoa/autofill/layout_view.h"
18 #include "chrome/browser/ui/cocoa/autofill/simple_grid_layout.h"
19 #import "chrome/browser/ui/cocoa/image_button_cell.h"
20 #import "chrome/browser/ui/cocoa/menu_button.h"
21 #include "components/autofill/core/browser/autofill_type.h"
22 #include "grit/theme_resources.h"
23 #import "ui/base/cocoa/menu_controller.h"
24 #include "ui/base/l10n/l10n_util_mac.h"
25 #include "ui/base/models/combobox_model.h"
26 #include "ui/base/resource/resource_bundle.h"
30 // Constants used for layouting controls. These variables are copied from
31 // "ui/views/layout/layout_constants.h".
33 // Horizontal spacing between controls that are logically related.
34 const int kRelatedControlHorizontalSpacing = 8;
36 // Vertical spacing between controls that are logically related.
37 const int kRelatedControlVerticalSpacing = 8;
39 // TODO(estade): pull out these constants, and figure out better values
40 // for them. Note: These are duplicated from Views code.
42 // Fixed width for the section label.
43 const int kLabelWidth = 180;
45 // Padding between section label and section input.
46 const int kPadding = 30;
48 // Fixed width for the details section.
49 const int kDetailsWidth = 440;
51 // Top/bottom inset for contents of a detail section.
52 const size_t kDetailSectionInset = 10;
54 // Vertical padding around the section header.
55 const CGFloat kVerticalHeaderPadding = 6;
57 // Break suggestion text into two lines. TODO(groby): Should be on delegate.
58 void BreakSuggestionText(const string16& text,
61 // TODO(estade): does this localize well?
62 string16 line_return(base::ASCIIToUTF16("\n"));
63 size_t position = text.find(line_return);
64 if (position == string16::npos) {
68 *line1 = text.substr(0, position);
69 *line2 = text.substr(position + line_return.length());
73 // If the Autofill data comes from a credit card, make sure to overwrite the
74 // CC comboboxes (even if they already have something in them). If the
75 // Autofill data comes from an AutofillProfile, leave the comboboxes alone.
76 // TODO(groby): This kind of logic should _really_ live on the delegate.
77 bool ShouldOverwriteComboboxes(autofill::DialogSection section,
78 autofill::ServerFieldType type) {
79 if (autofill::AutofillType(type).group() != autofill::CREDIT_CARD) {
83 if (section == autofill::SECTION_CC) {
87 return section == autofill::SECTION_CC_BILLING;
90 bool CompareInputRows(const autofill::DetailInput* input1,
91 const autofill::DetailInput* input2) {
92 // Row ID -1 is sorted to the end of rows.
93 if (input2->row_id == -1)
95 return input2->row_id < input1->row_id;
100 @interface AutofillSectionContainer ()
102 // A text field has been edited or activated - inform the delegate that it's
103 // time to show a suggestion popup & possibly reset the validity of the input.
104 - (void)textfieldEditedOrActivated:(NSControl<AutofillInputField>*)field
107 // Convenience method to retrieve a field type via the control's tag.
108 - (autofill::ServerFieldType)fieldTypeForControl:(NSControl*)control;
110 // Find the DetailInput* associated with a field type.
111 - (const autofill::DetailInput*)detailInputForType:
112 (autofill::ServerFieldType)type;
114 // Takes an NSArray of controls and builds a DetailOutputMap from them.
115 // Translates between Cocoa code and delegate, essentially.
116 // All controls must inherit from NSControl and conform to AutofillInputView.
117 - (void)fillDetailOutputs:(autofill::DetailOutputMap*)outputs
118 fromControls:(NSArray*)controls;
120 // Updates input fields based on delegate status. If |shouldClobber| is YES,
121 // will clobber existing data and reset fields to the initial values.
122 - (void)updateAndClobber:(BOOL)shouldClobber;
124 // Create properly styled label for section. Autoreleased.
125 - (NSTextField*)makeDetailSectionLabel:(NSString*)labelText;
127 // Create a button offering input suggestions.
128 - (MenuButton*)makeSuggestionButton;
130 // Create a view with all inputs requested by |delegate_|. Autoreleased.
131 - (LayoutView*)makeInputControls;
135 @implementation AutofillSectionContainer
137 @synthesize section = section_;
138 @synthesize validationDelegate = validationDelegate_;
140 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate
141 forSection:(autofill::DialogSection)section {
142 if (self = [super init]) {
144 delegate_ = delegate;
149 - (void)getInputs:(autofill::DetailOutputMap*)output {
150 [self fillDetailOutputs:output fromControls:[inputs_ subviews]];
153 // Note: This corresponds to Views' "UpdateDetailsGroupState".
154 - (void)modelChanged {
155 ui::MenuModel* suggestionModel = delegate_->MenuModelForSection(section_);
156 menuController_.reset([[MenuController alloc] initWithModel:suggestionModel
157 useWithPopUpButtonCell:YES]);
158 NSMenu* menu = [menuController_ menu];
160 const BOOL hasSuggestions = [menu numberOfItems] > 0;
161 [suggestButton_ setHidden:!hasSuggestions];
163 [suggestButton_ setAttachedMenu:menu];
165 [self updateSuggestionState];
167 // TODO(groby): "Save in Chrome" handling.
169 if (![[self view] isHidden])
170 [self validateFor:autofill::VALIDATE_EDIT];
172 // Always request re-layout on state change.
173 id delegate = [[view_ window] windowController];
174 if ([delegate respondsToSelector:@selector(requestRelayout)])
175 [delegate performSelector:@selector(requestRelayout)];
179 // Keep a list of weak pointers to DetailInputs.
180 const autofill::DetailInputs& inputs =
181 delegate_->RequestedFieldsForSection(section_);
182 for (size_t i = 0; i < inputs.size(); ++i) {
183 detailInputs_.push_back(&(inputs[i]));
186 inputs_.reset([[self makeInputControls] retain]);
187 string16 labelText = delegate_->LabelForSection(section_);
188 label_.reset([[self makeDetailSectionLabel:
189 base::SysUTF16ToNSString(labelText)] retain]);
191 suggestButton_.reset([[self makeSuggestionButton] retain]);
192 suggestContainer_.reset([[AutofillSuggestionContainer alloc] init]);
194 view_.reset([[AutofillSectionView alloc] initWithFrame:NSZeroRect]);
195 [self setView:view_];
196 [[self view] setSubviews:
197 @[label_, inputs_, [suggestContainer_ view], suggestButton_]];
199 if (section_ == autofill::SECTION_CC) {
200 // SECTION_CC *MUST* have a CREDIT_CARD_VERIFICATION_CODE input.
201 DCHECK([self detailInputForType:autofill::CREDIT_CARD_VERIFICATION_CODE]);
202 [[suggestContainer_ inputField] setTag:
203 autofill::CREDIT_CARD_VERIFICATION_CODE];
204 [[suggestContainer_ inputField] setDelegate:self];
210 - (NSSize)preferredSize {
211 if ([view_ isHidden])
214 NSSize labelSize = [label_ frame].size; // Assumes sizeToFit was called.
215 CGFloat controlHeight = [inputs_ preferredHeightForWidth:kDetailsWidth];
216 if (showSuggestions_)
217 controlHeight = [suggestContainer_ preferredSize].height;
219 return NSMakeSize(kDetailsWidth,
220 labelSize.height + kVerticalHeaderPadding +
221 controlHeight + 2 * kDetailSectionInset);
224 - (void)performLayout {
225 if ([view_ isHidden])
228 NSSize buttonSize = [suggestButton_ frame].size; // Assume sizeToFit.
229 NSSize labelSize = [label_ frame].size; // Assumes sizeToFit was called.
230 CGFloat controlHeight = [inputs_ preferredHeightForWidth:kDetailsWidth];
231 if (showSuggestions_)
232 controlHeight = [suggestContainer_ preferredSize].height;
234 NSRect viewFrame = NSZeroRect;
235 viewFrame.size = [self preferredSize];
237 NSRect contentFrame = NSInsetRect(viewFrame, 0, kDetailSectionInset);
238 NSRect controlFrame, labelFrame, buttonFrame;
240 // Label is top left, suggestion button is top right, controls are below that.
241 NSDivideRect(contentFrame, &labelFrame, &controlFrame,
242 kVerticalHeaderPadding + labelSize.height, NSMaxYEdge);
243 NSDivideRect(labelFrame, &buttonFrame, &labelFrame,
244 buttonSize.width, NSMaxXEdge);
246 labelFrame = NSOffsetRect(labelFrame, 0, kVerticalHeaderPadding);
247 labelFrame.size = labelSize;
249 buttonFrame = NSOffsetRect(buttonFrame, 0, 5);
250 buttonFrame.size = buttonSize;
252 if (showSuggestions_) {
253 [[suggestContainer_ view] setFrame:controlFrame];
254 [suggestContainer_ performLayout];
256 [inputs_ setFrame:controlFrame];
258 [label_ setFrame:labelFrame];
259 [suggestButton_ setFrame:buttonFrame];
260 [inputs_ setHidden:showSuggestions_];
261 [[suggestContainer_ view] setHidden:!showSuggestions_];
262 [view_ setFrameSize:viewFrame.size];
265 - (void)fieldBecameFirstResponder:(NSControl<AutofillInputField>*)field {
266 [self textfieldEditedOrActivated:field edited:NO];
267 [validationDelegate_ updateMessageForField:field];
270 - (void)didChange:(id)sender {
271 [self textfieldEditedOrActivated:sender edited:YES];
274 - (void)didEndEditing:(id)sender {
275 [self validateFor:autofill::VALIDATE_EDIT];
278 - (void)updateSuggestionState {
279 const autofill::SuggestionState& suggestionState =
280 delegate_->SuggestionStateForSection(section_);
281 // TODO(estade): use |vertically_compact_text| when it fits.
282 const base::string16& text = suggestionState.horizontally_compact_text;
283 showSuggestions_ = suggestionState.visible;
285 base::string16 line1;
286 base::string16 line2;
287 BreakSuggestionText(text, &line1, &line2);
288 [suggestContainer_ setSuggestionText:base::SysUTF16ToNSString(line1)
289 line2:base::SysUTF16ToNSString(line2)];
290 [suggestContainer_ setIcon:suggestionState.icon.AsNSImage()];
291 if (!suggestionState.extra_text.empty()) {
292 NSString* extraText =
293 base::SysUTF16ToNSString(suggestionState.extra_text);
294 NSImage* extraIcon = suggestionState.extra_icon.AsNSImage();
295 [suggestContainer_ showInputField:extraText withIcon:extraIcon];
297 [view_ setShouldHighlightOnHover:showSuggestions_];
298 if (showSuggestions_)
299 [view_ setClickTarget:suggestButton_];
301 [view_ setClickTarget:nil];
302 [view_ setHidden:!delegate_->SectionIsActive(section_)];
306 [self updateAndClobber:YES];
309 - (void)fillForInput:(const autofill::DetailInput&)input {
310 // Make sure to overwrite the originating input if it is a text field.
311 AutofillTextField* field =
312 base::mac::ObjCCast<AutofillTextField>([inputs_ viewWithTag:input.type]);
313 [field setFieldValue:@""];
315 if (ShouldOverwriteComboboxes(section_, input.type)) {
316 for (NSControl* control in [inputs_ subviews]) {
317 AutofillPopUpButton* popup =
318 base::mac::ObjCCast<AutofillPopUpButton>(control);
320 autofill::ServerFieldType fieldType =
321 [self fieldTypeForControl:popup];
322 if (autofill::AutofillType(fieldType).group() ==
323 autofill::CREDIT_CARD) {
324 ui::ComboboxModel* model =
325 delegate_->ComboboxModelForAutofillType(fieldType);
327 [popup selectItemAtIndex:model->GetDefaultIndex()];
333 [self updateAndClobber:NO];
336 - (BOOL)validateFor:(autofill::ValidationType)validationType {
337 NSArray* fields = nil;
338 if (![inputs_ isHidden]) {
339 fields = [inputs_ subviews];
340 } else if (section_ == autofill::SECTION_CC) {
341 fields = @[ [suggestContainer_ inputField] ];
344 // Ensure only editable fields are validated.
345 fields = [fields filteredArrayUsingPredicate:
346 [NSPredicate predicateWithBlock:
347 ^BOOL(NSControl<AutofillInputField>* field, NSDictionary* bindings) {
348 return [field isEnabled];
351 autofill::DetailOutputMap detailOutputs;
352 [self fillDetailOutputs:&detailOutputs fromControls:fields];
353 autofill::ValidityMessages messages = delegate_->InputsAreValid(
354 section_, detailOutputs);
356 for (NSControl<AutofillInputField>* input in fields) {
357 const autofill::ServerFieldType type = [self fieldTypeForControl:input];
358 const autofill::ValidityMessage& message =
359 messages.GetMessageOrDefault(type);
360 if (validationType != autofill::VALIDATE_FINAL && !message.sure)
362 [input setValidityMessage:base::SysUTF16ToNSString(message.text)];
363 [validationDelegate_ updateMessageForField:input];
366 return !messages.HasErrors();
369 #pragma mark Internal API for AutofillSectionContainer.
371 - (void)textfieldEditedOrActivated:(NSControl<AutofillInputField>*)field
372 edited:(BOOL)edited {
373 AutofillTextField* textfield =
374 base::mac::ObjCCastStrict<AutofillTextField>(field);
376 // This only applies to textfields.
380 autofill::ServerFieldType type = [self fieldTypeForControl:field];
381 string16 fieldValue = base::SysNSStringToUTF16([textfield fieldValue]);
383 // Get the frame rectangle for the designated field, in screen coordinates.
384 NSRect textFrameInScreen = [field convertRect:[field bounds] toView:nil];
385 textFrameInScreen.origin =
386 [[field window] convertBaseToScreen:textFrameInScreen.origin];
388 // And adjust for gfx::Rect being flipped compared to OSX coordinates.
389 NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
390 textFrameInScreen.origin.y =
391 NSMaxY([screen frame]) - NSMaxY(textFrameInScreen);
392 gfx::Rect textFrameRect(NSRectToCGRect(textFrameInScreen));
394 delegate_->UserEditedOrActivatedInput(section_,
395 [self detailInputForType:type],
401 // If the field is marked as invalid, check if the text is now valid.
402 // Many fields (i.e. CC#) are invalid for most of the duration of editing,
403 // so flagging them as invalid prematurely is not helpful. However,
404 // correcting a minor mistake (i.e. a wrong CC digit) should immediately
405 // result in validation - positive user feedback.
406 if ([textfield invalid] && edited) {
407 string16 message = delegate_->InputValidityMessage(section_,
410 [textfield setValidityMessage:base::SysUTF16ToNSString(message)];
411 [validationDelegate_ updateMessageForField:textfield];
413 // If the field transitioned from invalid to valid, re-validate the group,
414 // since inter-field checks become meaningful with valid fields.
415 if (![textfield invalid])
416 [self validateFor:autofill::VALIDATE_EDIT];
419 // Update the icon for the textfield.
420 gfx::Image icon = delegate_->IconForField(type, fieldValue);
421 if (!icon.IsEmpty()) {
422 [[textfield cell] setIcon:icon.ToNSImage()];
426 - (autofill::ServerFieldType)fieldTypeForControl:(NSControl*)control {
427 DCHECK([control tag]);
428 return static_cast<autofill::ServerFieldType>([control tag]);
431 - (const autofill::DetailInput*)detailInputForType:
432 (autofill::ServerFieldType)type {
433 for (size_t i = 0; i < detailInputs_.size(); ++i) {
434 if (detailInputs_[i]->type == type)
435 return detailInputs_[i];
437 // TODO(groby): Needs to be NOTREACHED. Can't, due to the fact that tests
438 // blindly call setFieldValue:forInput:, even for non-existing inputs.
442 - (void)fillDetailOutputs:(autofill::DetailOutputMap*)outputs
443 fromControls:(NSArray*)controls {
444 for (NSControl<AutofillInputField>* input in controls) {
445 DCHECK([input isKindOfClass:[NSControl class]]);
446 DCHECK([input conformsToProtocol:@protocol(AutofillInputField)]);
447 autofill::ServerFieldType fieldType = [self fieldTypeForControl:input];
448 DCHECK([self detailInputForType:fieldType]);
449 NSString* value = [input fieldValue];
450 outputs->insert(std::make_pair([self detailInputForType:fieldType],
451 base::SysNSStringToUTF16(value)));
455 - (NSTextField*)makeDetailSectionLabel:(NSString*)labelText {
456 base::scoped_nsobject<NSTextField> label([[NSTextField alloc] init]);
458 [[NSFontManager sharedFontManager] convertFont:[label font]
459 toHaveTrait:NSBoldFontMask]];
460 [label setStringValue:labelText];
461 [label setEditable:NO];
462 [label setBordered:NO];
463 [label setDrawsBackground:NO];
465 return label.autorelease();
468 - (void)updateAndClobber:(BOOL)shouldClobber {
469 const autofill::DetailInputs& updatedInputs =
470 delegate_->RequestedFieldsForSection(section_);
472 for (autofill::DetailInputs::const_iterator iter = updatedInputs.begin();
473 iter != updatedInputs.end();
475 NSControl<AutofillInputField>* field = [inputs_ viewWithTag:iter->type];
478 [field setEnabled:iter->editable];
480 if (shouldClobber || [field isDefault]) {
481 [field setFieldValue:base::SysUTF16ToNSString(iter->initial_value)];
482 AutofillTextField* textField =
483 base::mac::ObjCCast<AutofillTextField>(field);
486 delegate_->IconForField(iter->type, iter->initial_value);
487 if (!icon.IsEmpty()) {
488 [[textField cell] setIcon:icon.ToNSImage()];
493 [field setValidityMessage:@""];
498 - (MenuButton*)makeSuggestionButton {
499 base::scoped_nsobject<MenuButton> button([[MenuButton alloc] init]);
501 [button setOpenMenuOnClick:YES];
502 [button setBordered:NO];
503 [button setShowsBorderOnlyWhileMouseInside:YES];
505 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
507 rb.GetNativeImageNamed(IDR_AUTOFILL_DIALOG_MENU_BUTTON).ToNSImage();
508 [[button cell] setImage:image
509 forButtonState:image_button_cell::kDefaultState];
510 image = rb.GetNativeImageNamed(IDR_AUTOFILL_DIALOG_MENU_BUTTON_H).
512 [[button cell] setImage:image
513 forButtonState:image_button_cell::kHoverState];
514 image = rb.GetNativeImageNamed(IDR_AUTOFILL_DIALOG_MENU_BUTTON_P).
516 [[button cell] setImage:image
517 forButtonState:image_button_cell::kPressedState];
518 image = rb.GetNativeImageNamed(IDR_AUTOFILL_DIALOG_MENU_BUTTON_D).
520 [[button cell] setImage:image
521 forButtonState:image_button_cell::kDisabledState];
523 // ImageButtonCell's cellSize is not working. (http://crbug.com/298501)
524 [button setFrameSize:[image size]];
525 return button.autorelease();
528 // TODO(estade): we should be using Chrome-style constrained window padding
530 - (LayoutView*)makeInputControls {
531 base::scoped_nsobject<LayoutView> view([[LayoutView alloc] init]);
532 [view setLayoutManager:
533 scoped_ptr<SimpleGridLayout>(new SimpleGridLayout(view))];
534 SimpleGridLayout* layout = [view layoutManager];
536 // Reverse order of rows, but keep order of fields stable. stable_sort
537 // guarantees that field order within a row is not affected.
538 // Necessary since OSX builds forms from the bottom left.
540 detailInputs_.begin(), detailInputs_.end(), CompareInputRows);
541 for (size_t i = 0; i < detailInputs_.size(); ++i) {
542 const autofill::DetailInput& input = *detailInputs_[i];
543 int kColumnSetId = input.row_id;
544 ColumnSet* columnSet = layout->GetColumnSet(kColumnSetId);
546 // Create a new column set and row.
547 columnSet = layout->AddColumnSet(kColumnSetId);
548 if (i != 0 && kColumnSetId != -1)
549 layout->AddPaddingRow(kRelatedControlVerticalSpacing);
550 layout->StartRow(0, kColumnSetId);
552 // Add a new column to existing row.
553 columnSet->AddPaddingColumn(kRelatedControlHorizontalSpacing);
554 // Must explicitly skip the padding column since we've already started
556 layout->SkipColumns(1);
559 columnSet->AddColumn(input.expand_weight ? input.expand_weight : 1.0f);
561 ui::ComboboxModel* inputModel =
562 delegate_->ComboboxModelForAutofillType(input.type);
563 base::scoped_nsprotocol<NSControl<AutofillInputField>*> control;
565 base::scoped_nsobject<AutofillPopUpButton> popup(
566 [[AutofillPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]);
567 for (int i = 0; i < inputModel->GetItemCount(); ++i) {
568 [popup addItemWithTitle:
569 base::SysUTF16ToNSString(inputModel->GetItemAt(i))];
571 [popup setDefaultValue:base::SysUTF16ToNSString(
572 inputModel->GetItemAt(inputModel->GetDefaultIndex()))];
573 control.reset(popup.release());
575 base::scoped_nsobject<AutofillTextField> field(
576 [[AutofillTextField alloc] init]);
577 [[field cell] setPlaceholderString:
578 l10n_util::GetNSStringWithFixup(input.placeholder_text_rid)];
579 [[field cell] setIcon:
580 delegate_->IconForField(
581 input.type, input.initial_value).AsNSImage()];
582 [field setDefaultValue:@""];
583 control.reset(field.release());
585 [control setFieldValue:base::SysUTF16ToNSString(input.initial_value)];
587 [control setTag:input.type];
588 [control setDelegate:self];
589 // Hide away fields that cannot be edited.
590 if (kColumnSetId == -1) {
591 [control setFrame:NSZeroRect];
592 [control setHidden:YES];
594 layout->AddView(control);
597 return view.autorelease();
603 @implementation AutofillSectionContainer (ForTesting)
605 - (NSControl*)getField:(autofill::ServerFieldType)type {
606 return [inputs_ viewWithTag:type];
609 - (void)setFieldValue:(NSString*)text
610 forInput:(const autofill::DetailInput&)input {
611 if ([self detailInputForType:input.type] != &input)
614 NSControl<AutofillInputField>* field = [inputs_ viewWithTag:input.type];
615 [field setFieldValue:text];
618 - (void)setSuggestionFieldValue:(NSString*)text {
619 [[suggestContainer_ inputField] setFieldValue:text];
622 - (void)activateFieldForInput:(const autofill::DetailInput&)input {
623 if ([self detailInputForType:input.type] != &input)
626 NSControl<AutofillInputField>* field = [inputs_ viewWithTag:input.type];
627 [[field window] makeFirstResponder:field];