Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / autofill / autofill_section_container.mm
blobe01730af0540c32e6939a3e2dcf62f3ed9233a41
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"
7 #include <algorithm>
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 #include "chrome/browser/ui/chrome_style.h"
14 #import "chrome/browser/ui/cocoa/autofill/autofill_pop_up_button.h"
15 #import "chrome/browser/ui/cocoa/autofill/autofill_section_view.h"
16 #import "chrome/browser/ui/cocoa/autofill/autofill_suggestion_container.h"
17 #import "chrome/browser/ui/cocoa/autofill/autofill_textfield.h"
18 #import "chrome/browser/ui/cocoa/autofill/autofill_tooltip_controller.h"
19 #import "chrome/browser/ui/cocoa/autofill/layout_view.h"
20 #include "chrome/browser/ui/cocoa/autofill/simple_grid_layout.h"
21 #import "chrome/browser/ui/cocoa/image_button_cell.h"
22 #import "chrome/browser/ui/cocoa/menu_button.h"
23 #include "components/autofill/core/browser/autofill_type.h"
24 #include "content/public/browser/native_web_keyboard_event.h"
25 #include "grit/theme_resources.h"
26 #import "ui/base/cocoa/menu_controller.h"
27 #include "ui/base/l10n/l10n_util_mac.h"
28 #include "ui/base/models/combobox_model.h"
29 #include "ui/base/resource/resource_bundle.h"
31 namespace {
33 // Constants used for layouting controls. These variables are copied from
34 // "ui/views/layout/layout_constants.h".
36 // Horizontal spacing between controls that are logically related.
37 const int kRelatedControlHorizontalSpacing = 8;
39 // Vertical spacing between controls that are logically related.
40 const int kRelatedControlVerticalSpacing = 8;
42 // TODO(estade): pull out these constants, and figure out better values
43 // for them. Note: These are duplicated from Views code.
45 // Fixed width for the details section.
46 const int kDetailsWidth = 440;
48 // Top/bottom inset for contents of a detail section.
49 const size_t kDetailSectionInset = 10;
51 // Vertical padding around the section header.
52 const CGFloat kVerticalHeaderPadding = 6;
54 // If the Autofill data comes from a credit card, make sure to overwrite the
55 // CC comboboxes (even if they already have something in them). If the
56 // Autofill data comes from an AutofillProfile, leave the comboboxes alone.
57 // TODO(groby): This kind of logic should _really_ live on the delegate.
58 bool ShouldOverwriteComboboxes(autofill::DialogSection section,
59                                autofill::ServerFieldType type) {
60   if (autofill::AutofillType(type).group() != autofill::CREDIT_CARD) {
61     return false;
62   }
64   if (section == autofill::SECTION_CC) {
65     return true;
66   }
68   return section == autofill::SECTION_CC_BILLING;
71 }  // namespace
73 @interface AutofillSectionContainer ()
75 // An input field has been edited or activated - inform the delegate and
76 // possibly reset the validity of the input (if it's a textfield).
77 - (void)fieldEditedOrActivated:(NSControl<AutofillInputField>*)field
78                         edited:(BOOL)edited;
80 // Convenience method to retrieve a field type via the control's tag.
81 - (autofill::ServerFieldType)fieldTypeForControl:(NSControl*)control;
83 // Find the DetailInput* associated with a field type.
84 - (const autofill::DetailInput*)detailInputForType:
85     (autofill::ServerFieldType)type;
87 // Takes an NSArray of controls and builds a FieldValueMap from them.
88 // Translates between Cocoa code and delegate, essentially.
89 // All controls must inherit from NSControl and conform to AutofillInputView.
90 - (void)fillDetailOutputs:(autofill::FieldValueMap*)outputs
91              fromControls:(NSArray*)controls;
93 // Updates input fields based on delegate status. If |shouldClobber| is YES,
94 // will clobber existing data and reset fields to the initial values.
95 - (void)updateAndClobber:(BOOL)shouldClobber;
97 // Return YES if this is a section that contains CC info. (And, more
98 // importantly, a potential CVV field)
99 - (BOOL)isCreditCardSection;
101 // Create properly styled label for section. Autoreleased.
102 - (NSTextField*)makeDetailSectionLabel:(NSString*)labelText;
104 // Create a button offering input suggestions.
105 - (MenuButton*)makeSuggestionButton;
107 // Create a view with all inputs requested by |delegate_| and resets |input_|.
108 - (void)makeInputControls;
110 // Refresh all field icons based on |delegate_| status.
111 - (void)updateFieldIcons;
113 // Refresh the enabled/disabled state of all input fields.
114 - (void)updateEditability;
116 @end
118 @implementation AutofillSectionContainer
120 @synthesize section = section_;
121 @synthesize validationDelegate = validationDelegate_;
123 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate
124             forSection:(autofill::DialogSection)section {
125   if (self = [super init]) {
126     section_ = section;
127     delegate_ = delegate;
128   }
129   return self;
132 - (void)getInputs:(autofill::FieldValueMap*)output {
133   [self fillDetailOutputs:output fromControls:[inputs_ subviews]];
136 // Note: This corresponds to Views' "UpdateDetailsGroupState".
137 - (void)modelChanged {
138   ui::MenuModel* suggestionModel = delegate_->MenuModelForSection(section_);
139   menuController_.reset([[MenuController alloc] initWithModel:suggestionModel
140                                        useWithPopUpButtonCell:YES]);
141   NSMenu* menu = [menuController_ menu];
143   const BOOL hasSuggestions = [menu numberOfItems] > 0;
144   [suggestButton_ setHidden:!hasSuggestions];
146   [suggestButton_ setAttachedMenu:menu];
148   [self updateSuggestionState];
150   if (![[self view] isHidden])
151     [self validateFor:autofill::VALIDATE_EDIT];
153   // Always request re-layout on state change.
154   [self requestRelayout];
157 - (void)requestRelayout {
158   id delegate = [[view_ window] windowController];
159   if ([delegate respondsToSelector:@selector(requestRelayout)])
160     [delegate performSelector:@selector(requestRelayout)];
163 - (void)loadView {
164   [self makeInputControls];
166   base::string16 labelText = delegate_->LabelForSection(section_);
167   label_.reset(
168       [[self makeDetailSectionLabel:base::SysUTF16ToNSString(labelText)]
169           retain]);
171   suggestButton_.reset([[self makeSuggestionButton] retain]);
172   suggestContainer_.reset([[AutofillSuggestionContainer alloc] init]);
174   view_.reset([[AutofillSectionView alloc] initWithFrame:NSZeroRect]);
175   [self setView:view_];
176   [view_ setSubviews:
177       @[label_, inputs_, [suggestContainer_ view], suggestButton_]];
178   if (tooltipController_) {
179     [view_ addSubview:[tooltipController_ view]
180            positioned:NSWindowAbove
181            relativeTo:inputs_];
182   }
184   if ([self isCreditCardSection]) {
185     // Credit card sections *MUST* have a CREDIT_CARD_VERIFICATION_CODE input.
186     DCHECK([self detailInputForType:autofill::CREDIT_CARD_VERIFICATION_CODE]);
187     [[suggestContainer_ inputField] setTag:
188         autofill::CREDIT_CARD_VERIFICATION_CODE];
189     [[suggestContainer_ inputField] setInputDelegate:self];
190   }
192   [self modelChanged];
195 - (NSSize)preferredSize {
196   if ([view_ isHidden])
197     return NSZeroSize;
199   NSSize labelSize = [label_ frame].size;  // Assumes sizeToFit was called.
200   CGFloat controlHeight = [inputs_ preferredHeightForWidth:kDetailsWidth];
201   if (showSuggestions_)
202     controlHeight = [suggestContainer_ preferredSize].height;
204   return NSMakeSize(kDetailsWidth + 2 * chrome_style::kHorizontalPadding,
205                     labelSize.height + kVerticalHeaderPadding +
206                         controlHeight + 2 * kDetailSectionInset);
209 - (void)performLayout {
210   if ([view_ isHidden])
211     return;
213   NSSize buttonSize = [suggestButton_ frame].size;  // Assume sizeToFit.
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   NSRect viewFrame = NSZeroRect;
220   viewFrame.size = [self preferredSize];
222   NSRect contentFrame = NSInsetRect(viewFrame,
223                                     chrome_style::kHorizontalPadding,
224                                     kDetailSectionInset);
225   NSRect controlFrame, labelFrame, buttonFrame;
227   // Label is top left, suggestion button is top right, controls are below that.
228   NSDivideRect(contentFrame, &labelFrame, &controlFrame,
229                kVerticalHeaderPadding + labelSize.height, NSMaxYEdge);
230   NSDivideRect(labelFrame, &buttonFrame, &labelFrame,
231                buttonSize.width, NSMaxXEdge);
233   labelFrame = NSOffsetRect(labelFrame, 0, kVerticalHeaderPadding);
234   labelFrame.size = labelSize;
236   buttonFrame = NSOffsetRect(buttonFrame, 0, 5);
237   buttonFrame.size = buttonSize;
239   if (showSuggestions_) {
240     [[suggestContainer_ view] setFrame:controlFrame];
241     [suggestContainer_ performLayout];
242   } else {
243     [inputs_ setFrame:controlFrame];
244   }
245   [label_ setFrame:labelFrame];
246   [suggestButton_ setFrame:buttonFrame];
247   [inputs_ setHidden:showSuggestions_];
248   [[suggestContainer_ view] setHidden:!showSuggestions_];
249   [view_ setFrameSize:viewFrame.size];
250   if (tooltipController_) {
251     [[tooltipController_ view] setHidden:showSuggestions_];
252     NSRect tooltipIconFrame = [tooltipField_ decorationFrame];
253     tooltipIconFrame.origin =
254         [[self view] convertPoint:tooltipIconFrame.origin
255                          fromView:[tooltipField_ superview]];
256     [[tooltipController_ view] setFrame:tooltipIconFrame];
257   }
260 - (KeyEventHandled)keyEvent:(NSEvent*)event forInput:(id)sender {
261   content::NativeWebKeyboardEvent webEvent(event);
263   // Only handle keyDown, to handle key repeats without duplicates.
264   if (webEvent.type != content::NativeWebKeyboardEvent::RawKeyDown)
265     return kKeyEventNotHandled;
267   // Allow the delegate to intercept key messages.
268   if (delegate_->HandleKeyPressEventInInput(webEvent))
269     return kKeyEventHandled;
270   return kKeyEventNotHandled;
273 - (void)onMouseDown:(NSControl<AutofillInputField>*)field {
274   [self fieldEditedOrActivated:field edited:NO];
275   [validationDelegate_ updateMessageForField:field];
278 - (void)fieldBecameFirstResponder:(NSControl<AutofillInputField>*)field {
279   [validationDelegate_ updateMessageForField:field];
282 - (void)didChange:(id)sender {
283   [self fieldEditedOrActivated:sender edited:YES];
286 - (void)didEndEditing:(id)sender {
287   delegate_->FocusMoved();
288   [validationDelegate_ hideErrorBubble];
289   [self validateFor:autofill::VALIDATE_EDIT];
290   [self updateEditability];
293 - (void)updateSuggestionState {
294   const autofill::SuggestionState& suggestionState =
295       delegate_->SuggestionStateForSection(section_);
296   showSuggestions_ = suggestionState.visible;
298   if (!suggestionState.extra_text.empty()) {
299     NSString* extraText =
300         base::SysUTF16ToNSString(suggestionState.extra_text);
301     NSImage* extraIcon = suggestionState.extra_icon.AsNSImage();
302     [suggestContainer_ showInputField:extraText withIcon:extraIcon];
303   }
305   // NOTE: It's important to set the input field, if there is one, _before_
306   // setting the suggestion text, since the suggestion container needs to
307   // account for the input field's width when deciding which of the two string
308   // representations to use.
309   NSString* verticallyCompactText =
310       base::SysUTF16ToNSString(suggestionState.vertically_compact_text);
311   NSString* horizontallyCompactText =
312       base::SysUTF16ToNSString(suggestionState.horizontally_compact_text);
313   [suggestContainer_
314       setSuggestionWithVerticallyCompactText:verticallyCompactText
315                      horizontallyCompactText:horizontallyCompactText
316                                         icon:suggestionState.icon.AsNSImage()
317                                     maxWidth:kDetailsWidth];
319   [view_ setShouldHighlightOnHover:showSuggestions_];
320   if (showSuggestions_)
321     [view_ setClickTarget:suggestButton_];
322   else
323     [view_ setClickTarget:nil];
324   [view_ setHidden:!delegate_->SectionIsActive(section_)];
327 - (void)update {
328   [self updateAndClobber:YES];
329   [view_ updateHoverState];
332 - (void)fillForType:(const autofill::ServerFieldType)type {
333   // Make sure to overwrite the originating input if it is a text field.
334   AutofillTextField* field =
335       base::mac::ObjCCast<AutofillTextField>([inputs_ viewWithTag:type]);
336   [field setFieldValue:@""];
338   if (ShouldOverwriteComboboxes(section_, type)) {
339     for (NSControl* control in [inputs_ subviews]) {
340       AutofillPopUpButton* popup =
341           base::mac::ObjCCast<AutofillPopUpButton>(control);
342       if (popup) {
343         autofill::ServerFieldType fieldType =
344             [self fieldTypeForControl:popup];
345         if (autofill::AutofillType(fieldType).group() ==
346                 autofill::CREDIT_CARD) {
347           ui::ComboboxModel* model =
348               delegate_->ComboboxModelForAutofillType(fieldType);
349           DCHECK(model);
350           [popup selectItemAtIndex:model->GetDefaultIndex()];
351         }
352       }
353     }
354   }
356   [self updateAndClobber:NO];
359 - (BOOL)validateFor:(autofill::ValidationType)validationType {
360   NSArray* fields = nil;
361   if (!showSuggestions_) {
362     fields = [inputs_ subviews];
363   } else if ([self isCreditCardSection]) {
364     if (![[suggestContainer_ inputField] isHidden])
365       fields = @[ [suggestContainer_ inputField] ];
366   }
368   // Ensure only editable fields are validated.
369   fields = [fields filteredArrayUsingPredicate:
370       [NSPredicate predicateWithBlock:
371           ^BOOL(NSControl<AutofillInputField>* field, NSDictionary* bindings) {
372               return [field isEnabled];
373           }]];
375   autofill::FieldValueMap detailOutputs;
376   [self fillDetailOutputs:&detailOutputs fromControls:fields];
377   autofill::ValidityMessages messages = delegate_->InputsAreValid(
378       section_, detailOutputs);
380   for (NSControl<AutofillInputField>* input in fields) {
381     const autofill::ValidityMessage& message =
382         messages.GetMessageOrDefault([self fieldTypeForControl:input]);
383     if (validationType != autofill::VALIDATE_FINAL && !message.sure)
384       continue;
385     [input setValidityMessage:base::SysUTF16ToNSString(message.text)];
386     [validationDelegate_ updateMessageForField:input];
387   }
389   return !messages.HasErrors();
392 - (NSString*)suggestionText {
393   return showSuggestions_ ? [[suggestContainer_ inputField] stringValue] : nil;
396 - (void)addInputsToArray:(NSMutableArray*)array {
397   [array addObjectsFromArray:[inputs_ subviews]];
399   // Only credit card sections can have a suggestion input.
400   if ([self isCreditCardSection])
401     [array addObject:[suggestContainer_ inputField]];
404 #pragma mark Internal API for AutofillSectionContainer.
406 - (void)fieldEditedOrActivated:(NSControl<AutofillInputField>*)field
407                         edited:(BOOL)edited {
408   autofill::ServerFieldType type = [self fieldTypeForControl:field];
409   base::string16 fieldValue = base::SysNSStringToUTF16([field fieldValue]);
411   // Get the frame rectangle for the designated field, in screen coordinates.
412   NSRect textFrameInScreen = [field convertRect:[field bounds] toView:nil];
413   textFrameInScreen.origin =
414       [[field window] convertBaseToScreen:textFrameInScreen.origin];
416   // And adjust for gfx::Rect being flipped compared to OSX coordinates.
417   NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
418   textFrameInScreen.origin.y =
419       NSMaxY([screen frame]) - NSMaxY(textFrameInScreen);
420   gfx::Rect textFrameRect(NSRectToCGRect(textFrameInScreen));
422   delegate_->UserEditedOrActivatedInput(section_,
423                                         type,
424                                         [self view],
425                                         textFrameRect,
426                                         fieldValue,
427                                         edited);
429   AutofillTextField* textfield = base::mac::ObjCCast<AutofillTextField>(field);
430   if (!textfield)
431     return;
433   // If the field is marked as invalid, check if the text is now valid. Many
434   // fields (i.e. CC#) are invalid for most of the duration of editing, so
435   // flagging them as invalid prematurely is not helpful. However, correcting a
436   // minor mistake (i.e. a wrong CC digit) should immediately result in
437   // validation - positive user feedback.
438   if ([textfield invalid] && edited) {
439     base::string16 message = delegate_->InputValidityMessage(section_,
440                                                              type,
441                                                              fieldValue);
442     [textfield setValidityMessage:base::SysUTF16ToNSString(message)];
444     // If the field transitioned from invalid to valid, re-validate the group,
445     // since inter-field checks become meaningful with valid fields.
446     if (![textfield invalid])
447       [self validateFor:autofill::VALIDATE_EDIT];
449     // The validity message has potentially changed - notify the error bubble.
450     [validationDelegate_ updateMessageForField:textfield];
451   }
453   // Update the icon if necessary.
454   if (delegate_->FieldControlsIcons(type))
455     [self updateFieldIcons];
456   [self updateEditability];
459 - (autofill::ServerFieldType)fieldTypeForControl:(NSControl*)control {
460   DCHECK([control tag]);
461   return static_cast<autofill::ServerFieldType>([control tag]);
464 - (const autofill::DetailInput*)detailInputForType:
465     (autofill::ServerFieldType)type {
466   for (size_t i = 0; i < detailInputs_.size(); ++i) {
467     if (detailInputs_[i]->type == type)
468       return detailInputs_[i];
469   }
470   // TODO(groby): Needs to be NOTREACHED. Can't, due to the fact that tests
471   // blindly call setFieldValue:forType:, even for non-existing inputs.
472   return NULL;
475 - (void)fillDetailOutputs:(autofill::FieldValueMap*)outputs
476              fromControls:(NSArray*)controls {
477   for (NSControl<AutofillInputField>* input in controls) {
478     DCHECK([input isKindOfClass:[NSControl class]]);
479     DCHECK([input conformsToProtocol:@protocol(AutofillInputField)]);
480     outputs->insert(std::make_pair(
481         [self fieldTypeForControl:input],
482         base::SysNSStringToUTF16([input fieldValue])));
483   }
486 - (NSTextField*)makeDetailSectionLabel:(NSString*)labelText {
487   base::scoped_nsobject<NSTextField> label([[NSTextField alloc] init]);
488   [label setFont:
489       [[NSFontManager sharedFontManager] convertFont:[label font]
490                                          toHaveTrait:NSBoldFontMask]];
491   [label setStringValue:labelText];
492   [label setEditable:NO];
493   [label setBordered:NO];
494   [label setDrawsBackground:NO];
495   [label sizeToFit];
496   return label.autorelease();
499 - (void)updateAndClobber:(BOOL)shouldClobber {
500   if (shouldClobber) {
501     // Remember which one of the inputs was first responder so focus can be
502     // restored after the inputs are rebuilt.
503     NSView* firstResponderView =
504         base::mac::ObjCCast<NSView>([[inputs_ window] firstResponder]);
505     autofill::ServerFieldType type = autofill::UNKNOWN_TYPE;
506     for (NSControl* field in [inputs_ subviews]) {
507       if ([firstResponderView isDescendantOf:field]) {
508         type = [self fieldTypeForControl:field];
509         break;
510       }
511     }
513     [self makeInputControls];
515     if (type != autofill::UNKNOWN_TYPE) {
516       NSView* view = [inputs_ viewWithTag:type];
517       if (view)
518         [[inputs_ window] makeFirstResponder:view];
519     }
520   } else {
521     const autofill::DetailInputs& updatedInputs =
522         delegate_->RequestedFieldsForSection(section_);
524     for (autofill::DetailInputs::const_iterator iter = updatedInputs.begin();
525          iter != updatedInputs.end();
526          ++iter) {
527       NSControl<AutofillInputField>* field = [inputs_ viewWithTag:iter->type];
528       DCHECK(field);
529       if ([field isDefault])
530         [field setFieldValue:base::SysUTF16ToNSString(iter->initial_value)];
531     }
532     [self updateFieldIcons];
533   }
535   [self updateEditability];
536   [self modelChanged];
539 - (BOOL)isCreditCardSection {
540   return section_ == autofill::SECTION_CC ||
541       section_ == autofill::SECTION_CC_BILLING;
544 - (MenuButton*)makeSuggestionButton {
545   base::scoped_nsobject<MenuButton> button([[MenuButton alloc] init]);
547   [button setOpenMenuOnClick:YES];
548   [button setBordered:NO];
549   [button setShowsBorderOnlyWhileMouseInside:YES];
551   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
552   NSImage* image =
553       rb.GetNativeImageNamed(IDR_AUTOFILL_DIALOG_MENU_BUTTON).ToNSImage();
554   [[button cell] setImage:image
555            forButtonState:image_button_cell::kDefaultState];
556   image = rb.GetNativeImageNamed(IDR_AUTOFILL_DIALOG_MENU_BUTTON_H).
557       ToNSImage();
558   [[button cell] setImage:image
559            forButtonState:image_button_cell::kHoverState];
560   image = rb.GetNativeImageNamed(IDR_AUTOFILL_DIALOG_MENU_BUTTON_P).
561       ToNSImage();
562   [[button cell] setImage:image
563            forButtonState:image_button_cell::kPressedState];
564   image = rb.GetNativeImageNamed(IDR_AUTOFILL_DIALOG_MENU_BUTTON_D).
565       ToNSImage();
566   [[button cell] setImage:image
567            forButtonState:image_button_cell::kDisabledState];
569   // ImageButtonCell's cellSize is not working. (http://crbug.com/298501)
570   [button setFrameSize:[image size]];
571   return button.autorelease();
574 // TODO(estade): we should be using Chrome-style constrained window padding
575 // values.
576 - (void)makeInputControls {
577   if (inputs_) {
578     // When |inputs_| is replaced in response to a country change, there's a
579     // didEndEditing dispatched that segfaults or DCHECKS() as it's operating on
580     // stale input fields. Nil out the input delegate so this doesn't happen.
581     for (NSControl<AutofillInputField>* input in [inputs_ subviews]) {
582       [input setInputDelegate:nil];
583     }
584   }
586   detailInputs_.clear();
588   // Keep a list of weak pointers to DetailInputs.
589   const autofill::DetailInputs& inputs =
590       delegate_->RequestedFieldsForSection(section_);
592   // Reverse the order of all the inputs.
593   for (int i = inputs.size() - 1; i >= 0; --i) {
594     detailInputs_.push_back(&(inputs[i]));
595   }
597   // Then right the reversal in each row.
598   std::vector<const autofill::DetailInput*>::iterator it;
599   for (it = detailInputs_.begin(); it < detailInputs_.end(); ++it) {
600     std::vector<const autofill::DetailInput*>::iterator start = it;
601     while (it != detailInputs_.end() &&
602            (*it)->length != autofill::DetailInput::LONG) {
603       ++it;
604     }
605     std::reverse(start, it);
606   }
608   base::scoped_nsobject<LayoutView> view([[LayoutView alloc] init]);
609   [view setLayoutManager:
610       scoped_ptr<SimpleGridLayout>(new SimpleGridLayout(view))];
611   SimpleGridLayout* layout = [view layoutManager];
613   int column_set_id = 0;
614   for (size_t i = 0; i < detailInputs_.size(); ++i) {
615     const autofill::DetailInput& input = *detailInputs_[i];
617     if (input.length == autofill::DetailInput::LONG)
618       ++column_set_id;
620     int kColumnSetId =
621         input.length == autofill::DetailInput::NONE ? -1 : column_set_id;
623     ColumnSet* columnSet = layout->GetColumnSet(kColumnSetId);
624     if (!columnSet) {
625       // Create a new column set and row.
626       columnSet = layout->AddColumnSet(kColumnSetId);
627       if (i != 0 && kColumnSetId != -1)
628         layout->AddPaddingRow(kRelatedControlVerticalSpacing);
629       layout->StartRow(0, kColumnSetId);
630     } else {
631       // Add a new column to existing row.
632       columnSet->AddPaddingColumn(kRelatedControlHorizontalSpacing);
633       // Must explicitly skip the padding column since we've already started
634       // adding views.
635       layout->SkipColumns(1);
636     }
638     columnSet->AddColumn(input.expand_weight ? input.expand_weight : 1.0f);
640     ui::ComboboxModel* inputModel =
641         delegate_->ComboboxModelForAutofillType(input.type);
642     base::scoped_nsprotocol<NSControl<AutofillInputField>*> control;
643     if (inputModel) {
644       base::scoped_nsobject<AutofillPopUpButton> popup(
645           [[AutofillPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]);
646       for (int i = 0; i < inputModel->GetItemCount(); ++i) {
647         if (!inputModel->IsItemSeparatorAt(i)) {
648           // Currently, the first item in |inputModel| is duplicated later in
649           // the list. The second item is a separator. Because NSPopUpButton
650           // de-duplicates, the menu's just left with a separator on the top of
651           // the list (with nothing it's separating). For that reason,
652           // separators are ignored on Mac for now. http://crbug.com/347653
653           [popup addItemWithTitle:
654               base::SysUTF16ToNSString(inputModel->GetItemAt(i))];
655         }
656       }
657       [popup setDefaultValue:base::SysUTF16ToNSString(
658           inputModel->GetItemAt(inputModel->GetDefaultIndex()))];
659       control.reset(popup.release());
660     } else {
661       base::scoped_nsobject<AutofillTextField> field(
662           [[AutofillTextField alloc] init]);
663       [field setIsMultiline:input.IsMultiline()];
664       [[field cell] setLineBreakMode:NSLineBreakByClipping];
665       [[field cell] setScrollable:YES];
666       [[field cell] setPlaceholderString:
667           l10n_util::FixUpWindowsStyleLabel(input.placeholder_text)];
668       NSString* tooltipText =
669           base::SysUTF16ToNSString(delegate_->TooltipForField(input.type));
670       // VoiceOver onlys seems to pick up the help message on [field cell]
671       // (rather than just field).
672       BOOL success = [[field cell]
673           accessibilitySetOverrideValue:tooltipText
674                            forAttribute:NSAccessibilityHelpAttribute];
675       DCHECK(success);
676       if ([tooltipText length] > 0) {
677         if (!tooltipController_) {
678           tooltipController_.reset(
679               [[AutofillTooltipController alloc]
680                    initWithArrowLocation:info_bubble::kTopRight]);
681         }
682         tooltipField_ = field.get();
683         NSImage* icon =
684             ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
685                 IDR_AUTOFILL_TOOLTIP_ICON).ToNSImage();
686         [tooltipController_ setImage:icon];
687         [tooltipController_ setMessage:tooltipText];
688         [[field cell] setDecorationSize:[icon size]];
689       }
690       [field setDefaultValue:@""];
691       control.reset(field.release());
692     }
693     [control setTag:input.type];
694     [control setFieldValue:base::SysUTF16ToNSString(input.initial_value)];
695     [control sizeToFit];
696     [control setFrame:NSIntegralRect([control frame])];
697     [control setInputDelegate:self];
698     // Hide away fields that cannot be edited.
699     if (kColumnSetId == -1) {
700       [control setFrame:NSZeroRect];
701       [control setHidden:YES];
702     }
703     layout->AddView(control);
705     if (input.length == autofill::DetailInput::LONG ||
706         input.length == autofill::DetailInput::SHORT_EOL) {
707       ++column_set_id;
708     }
709   }
711   if (inputs_) {
712     [[self view] replaceSubview:inputs_ with:view];
713     [self requestRelayout];
714   }
716   inputs_ = view;
717   [self updateFieldIcons];
720 - (void)updateFieldIcons {
721   autofill::FieldValueMap fieldValues;
722   for (NSControl<AutofillInputField>* input in [inputs_ subviews]) {
723     DCHECK([input isKindOfClass:[NSControl class]]);
724     DCHECK([input conformsToProtocol:@protocol(AutofillInputField)]);
725     autofill::ServerFieldType fieldType = [self fieldTypeForControl:input];
726     NSString* value = [input fieldValue];
727     fieldValues[fieldType] = base::SysNSStringToUTF16(value);
728   }
730   autofill::FieldIconMap fieldIcons = delegate_->IconsForFields(fieldValues);
731   for (autofill::FieldIconMap::const_iterator iter = fieldIcons.begin();
732        iter!= fieldIcons.end(); ++iter) {
733     AutofillTextField* textfield = base::mac::ObjCCastStrict<AutofillTextField>(
734         [inputs_ viewWithTag:iter->first]);
735     [[textfield cell] setIcon:iter->second.ToNSImage()];
736   }
739 - (void)updateEditability {
740   base::scoped_nsobject<NSMutableArray> controls([[NSMutableArray alloc] init]);
741   [self addInputsToArray:controls];
742   for (NSControl<AutofillInputField>* control in controls.get()) {
743     autofill::ServerFieldType type = [self fieldTypeForControl:control];
744     const autofill::DetailInput* input = [self detailInputForType:type];
745     [control setEnabled:delegate_->InputIsEditable(*input, section_)];
746   }
749 @end
752 @implementation AutofillSectionContainer (ForTesting)
754 - (NSControl*)getField:(autofill::ServerFieldType)type {
755   return [inputs_ viewWithTag:type];
758 - (void)setFieldValue:(NSString*)text
759               forType:(autofill::ServerFieldType)type {
760   NSControl<AutofillInputField>* field = [inputs_ viewWithTag:type];
761   if (field)
762     [field setFieldValue:text];
765 - (void)setSuggestionFieldValue:(NSString*)text {
766   [[suggestContainer_ inputField] setFieldValue:text];
769 - (void)activateFieldForType:(autofill::ServerFieldType)type {
770   NSControl<AutofillInputField>* field = [inputs_ viewWithTag:type];
771   if (field) {
772     [[field window] makeFirstResponder:field];
773     [self fieldEditedOrActivated:field edited:NO];
774   }
777 @end