Home | History | Annotate | Download | only in autofill
      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.
      4 
      5 #import "chrome/browser/ui/cocoa/autofill/autofill_section_container.h"
      6 
      7 #include <algorithm>
      8 
      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"
     30 
     31 namespace {
     32 
     33 // Constants used for layouting controls. These variables are copied from
     34 // "ui/views/layout/layout_constants.h".
     35 
     36 // Horizontal spacing between controls that are logically related.
     37 const int kRelatedControlHorizontalSpacing = 8;
     38 
     39 // Vertical spacing between controls that are logically related.
     40 const int kRelatedControlVerticalSpacing = 8;
     41 
     42 // TODO(estade): pull out these constants, and figure out better values
     43 // for them. Note: These are duplicated from Views code.
     44 
     45 // Fixed width for the details section.
     46 const int kDetailsWidth = 440;
     47 
     48 // Top/bottom inset for contents of a detail section.
     49 const size_t kDetailSectionInset = 10;
     50 
     51 // Vertical padding around the section header.
     52 const CGFloat kVerticalHeaderPadding = 6;
     53 
     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   }
     63 
     64   if (section == autofill::SECTION_CC) {
     65     return true;
     66   }
     67 
     68   return section == autofill::SECTION_CC_BILLING;
     69 }
     70 
     71 }  // namespace
     72 
     73 @interface AutofillSectionContainer ()
     74 
     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;
     79 
     80 // Convenience method to retrieve a field type via the control's tag.
     81 - (autofill::ServerFieldType)fieldTypeForControl:(NSControl*)control;
     82 
     83 // Find the DetailInput* associated with a field type.
     84 - (const autofill::DetailInput*)detailInputForType:
     85     (autofill::ServerFieldType)type;
     86 
     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;
     92 
     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;
     96 
     97 // Return YES if this is a section that contains CC info. (And, more
     98 // importantly, a potential CVV field)
     99 - (BOOL)isCreditCardSection;
    100 
    101 // Create properly styled label for section. Autoreleased.
    102 - (NSTextField*)makeDetailSectionLabel:(NSString*)labelText;
    103 
    104 // Create a button offering input suggestions.
    105 - (MenuButton*)makeSuggestionButton;
    106 
    107 // Create a view with all inputs requested by |delegate_| and resets |input_|.
    108 - (void)makeInputControls;
    109 
    110 // Refresh all field icons based on |delegate_| status.
    111 - (void)updateFieldIcons;
    112 
    113 // Refresh the enabled/disabled state of all input fields.
    114 - (void)updateEditability;
    115 
    116 @end
    117 
    118 @implementation AutofillSectionContainer
    119 
    120 @synthesize section = section_;
    121 @synthesize validationDelegate = validationDelegate_;
    122 
    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;
    130 }
    131 
    132 - (void)getInputs:(autofill::FieldValueMap*)output {
    133   [self fillDetailOutputs:output fromControls:[inputs_ subviews]];
    134 }
    135 
    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];
    142 
    143   const BOOL hasSuggestions = [menu numberOfItems] > 0;
    144   [suggestButton_ setHidden:!hasSuggestions];
    145 
    146   [suggestButton_ setAttachedMenu:menu];
    147 
    148   [self updateSuggestionState];
    149 
    150   if (![[self view] isHidden])
    151     [self validateFor:autofill::VALIDATE_EDIT];
    152 
    153   // Always request re-layout on state change.
    154   [self requestRelayout];
    155 }
    156 
    157 - (void)requestRelayout {
    158   id delegate = [[view_ window] windowController];
    159   if ([delegate respondsToSelector:@selector(requestRelayout)])
    160     [delegate performSelector:@selector(requestRelayout)];
    161 }
    162 
    163 - (void)loadView {
    164   [self makeInputControls];
    165 
    166   base::string16 labelText = delegate_->LabelForSection(section_);
    167   label_.reset(
    168       [[self makeDetailSectionLabel:base::SysUTF16ToNSString(labelText)]
    169           retain]);
    170 
    171   suggestButton_.reset([[self makeSuggestionButton] retain]);
    172   suggestContainer_.reset([[AutofillSuggestionContainer alloc] init]);
    173 
    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   }
    183 
    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   }
    191 
    192   [self modelChanged];
    193 }
    194 
    195 - (NSSize)preferredSize {
    196   if ([view_ isHidden])
    197     return NSZeroSize;
    198 
    199   NSSize labelSize = [label_ frame].size;  // Assumes sizeToFit was called.
    200   CGFloat controlHeight = [inputs_ preferredHeightForWidth:kDetailsWidth];
    201   if (showSuggestions_)
    202     controlHeight = [suggestContainer_ preferredSize].height;
    203 
    204   return NSMakeSize(kDetailsWidth + 2 * chrome_style::kHorizontalPadding,
    205                     labelSize.height + kVerticalHeaderPadding +
    206                         controlHeight + 2 * kDetailSectionInset);
    207 }
    208 
    209 - (void)performLayout {
    210   if ([view_ isHidden])
    211     return;
    212 
    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;
    218 
    219   NSRect viewFrame = NSZeroRect;
    220   viewFrame.size = [self preferredSize];
    221 
    222   NSRect contentFrame = NSInsetRect(viewFrame,
    223                                     chrome_style::kHorizontalPadding,
    224                                     kDetailSectionInset);
    225   NSRect controlFrame, labelFrame, buttonFrame;
    226 
    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);
    232 
    233   labelFrame = NSOffsetRect(labelFrame, 0, kVerticalHeaderPadding);
    234   labelFrame.size = labelSize;
    235 
    236   buttonFrame = NSOffsetRect(buttonFrame, 0, 5);
    237   buttonFrame.size = buttonSize;
    238 
    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   }
    258 }
    259 
    260 - (KeyEventHandled)keyEvent:(NSEvent*)event forInput:(id)sender {
    261   content::NativeWebKeyboardEvent webEvent(event);
    262 
    263   // Only handle keyDown, to handle key repeats without duplicates.
    264   if (webEvent.type != content::NativeWebKeyboardEvent::RawKeyDown)
    265     return kKeyEventNotHandled;
    266 
    267   // Allow the delegate to intercept key messages.
    268   if (delegate_->HandleKeyPressEventInInput(webEvent))
    269     return kKeyEventHandled;
    270   return kKeyEventNotHandled;
    271 }
    272 
    273 - (void)onMouseDown:(NSControl<AutofillInputField>*)field {
    274   [self fieldEditedOrActivated:field edited:NO];
    275   [validationDelegate_ updateMessageForField:field];
    276 }
    277 
    278 - (void)fieldBecameFirstResponder:(NSControl<AutofillInputField>*)field {
    279   [validationDelegate_ updateMessageForField:field];
    280 }
    281 
    282 - (void)didChange:(id)sender {
    283   [self fieldEditedOrActivated:sender edited:YES];
    284 }
    285 
    286 - (void)didEndEditing:(id)sender {
    287   delegate_->FocusMoved();
    288   [validationDelegate_ hideErrorBubble];
    289   [self validateFor:autofill::VALIDATE_EDIT];
    290   [self updateEditability];
    291 }
    292 
    293 - (void)updateSuggestionState {
    294   const autofill::SuggestionState& suggestionState =
    295       delegate_->SuggestionStateForSection(section_);
    296   showSuggestions_ = suggestionState.visible;
    297 
    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   }
    304 
    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];
    318 
    319   [view_ setShouldHighlightOnHover:showSuggestions_];
    320   if (showSuggestions_)
    321     [view_ setClickTarget:suggestButton_];
    322   else
    323     [view_ setClickTarget:nil];
    324   [view_ setHidden:!delegate_->SectionIsActive(section_)];
    325 }
    326 
    327 - (void)update {
    328   [self updateAndClobber:YES];
    329   [view_ updateHoverState];
    330 }
    331 
    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:@""];
    337 
    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   }
    355 
    356   [self updateAndClobber:NO];
    357 }
    358 
    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   }
    367 
    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           }]];
    374 
    375   autofill::FieldValueMap detailOutputs;
    376   [self fillDetailOutputs:&detailOutputs fromControls:fields];
    377   autofill::ValidityMessages messages = delegate_->InputsAreValid(
    378       section_, detailOutputs);
    379 
    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   }
    388 
    389   return !messages.HasErrors();
    390 }
    391 
    392 - (NSString*)suggestionText {
    393   return showSuggestions_ ? [[suggestContainer_ inputField] stringValue] : nil;
    394 }
    395 
    396 - (void)addInputsToArray:(NSMutableArray*)array {
    397   [array addObjectsFromArray:[inputs_ subviews]];
    398 
    399   // Only credit card sections can have a suggestion input.
    400   if ([self isCreditCardSection])
    401     [array addObject:[suggestContainer_ inputField]];
    402 }
    403 
    404 #pragma mark Internal API for AutofillSectionContainer.
    405 
    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]);
    410 
    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];
    415 
    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));
    421 
    422   delegate_->UserEditedOrActivatedInput(section_,
    423                                         type,
    424                                         [self view],
    425                                         textFrameRect,
    426                                         fieldValue,
    427                                         edited);
    428 
    429   AutofillTextField* textfield = base::mac::ObjCCast<AutofillTextField>(field);
    430   if (!textfield)
    431     return;
    432 
    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)];
    443 
    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];
    448 
    449     // The validity message has potentially changed - notify the error bubble.
    450     [validationDelegate_ updateMessageForField:textfield];
    451   }
    452 
    453   // Update the icon if necessary.
    454   if (delegate_->FieldControlsIcons(type))
    455     [self updateFieldIcons];
    456   [self updateEditability];
    457 }
    458 
    459 - (autofill::ServerFieldType)fieldTypeForControl:(NSControl*)control {
    460   DCHECK([control tag]);
    461   return static_cast<autofill::ServerFieldType>([control tag]);
    462 }
    463 
    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;
    473 }
    474 
    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   }
    484 }
    485 
    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();
    497 }
    498 
    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     }
    512 
    513     [self makeInputControls];
    514 
    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_);
    523 
    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   }
    534 
    535   [self updateEditability];
    536   [self modelChanged];
    537 }
    538 
    539 - (BOOL)isCreditCardSection {
    540   return section_ == autofill::SECTION_CC ||
    541       section_ == autofill::SECTION_CC_BILLING;
    542 }
    543 
    544 - (MenuButton*)makeSuggestionButton {
    545   base::scoped_nsobject<MenuButton> button([[MenuButton alloc] init]);
    546 
    547   [button setOpenMenuOnClick:YES];
    548   [button setBordered:NO];
    549   [button setShowsBorderOnlyWhileMouseInside:YES];
    550 
    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];
    568 
    569   // ImageButtonCell's cellSize is not working. (http://crbug.com/298501)
    570   [button setFrameSize:[image size]];
    571   return button.autorelease();
    572 }
    573 
    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   }
    585 
    586   detailInputs_.clear();
    587 
    588   // Keep a list of weak pointers to DetailInputs.
    589   const autofill::DetailInputs& inputs =
    590       delegate_->RequestedFieldsForSection(section_);
    591 
    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   }
    596 
    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   }
    607 
    608   base::scoped_nsobject<LayoutView> view([[LayoutView alloc] init]);
    609   [view setLayoutManager:
    610       scoped_ptr<SimpleGridLayout>(new SimpleGridLayout(view))];
    611   SimpleGridLayout* layout = [view layoutManager];
    612 
    613   int column_set_id = 0;
    614   for (size_t i = 0; i < detailInputs_.size(); ++i) {
    615     const autofill::DetailInput& input = *detailInputs_[i];
    616 
    617     if (input.length == autofill::DetailInput::LONG)
    618       ++column_set_id;
    619 
    620     int kColumnSetId =
    621         input.length == autofill::DetailInput::NONE ? -1 : column_set_id;
    622 
    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     }
    637 
    638     columnSet->AddColumn(input.expand_weight ? input.expand_weight : 1.0f);
    639 
    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);
    704 
    705     if (input.length == autofill::DetailInput::LONG ||
    706         input.length == autofill::DetailInput::SHORT_EOL) {
    707       ++column_set_id;
    708     }
    709   }
    710 
    711   if (inputs_) {
    712     [[self view] replaceSubview:inputs_ with:view];
    713     [self requestRelayout];
    714   }
    715 
    716   inputs_ = view;
    717   [self updateFieldIcons];
    718 }
    719 
    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   }
    729 
    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   }
    737 }
    738 
    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   }
    747 }
    748 
    749 @end
    750 
    751 
    752 @implementation AutofillSectionContainer (ForTesting)
    753 
    754 - (NSControl*)getField:(autofill::ServerFieldType)type {
    755   return [inputs_ viewWithTag:type];
    756 }
    757 
    758 - (void)setFieldValue:(NSString*)text
    759               forType:(autofill::ServerFieldType)type {
    760   NSControl<AutofillInputField>* field = [inputs_ viewWithTag:type];
    761   if (field)
    762     [field setFieldValue:text];
    763 }
    764 
    765 - (void)setSuggestionFieldValue:(NSString*)text {
    766   [[suggestContainer_ inputField] setFieldValue:text];
    767 }
    768 
    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   }
    775 }
    776 
    777 @end
    778