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