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_details_container.h"
9 #include "base/mac/foundation_util.h"
10 #include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h"
11 #import "chrome/browser/ui/cocoa/autofill/autofill_bubble_controller.h"
12 #import "chrome/browser/ui/cocoa/autofill/autofill_section_container.h"
13 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
15 typedef BOOL (^FieldFilterBlock)(NSView<AutofillInputField>*);
17 @interface AutofillDetailsContainer ()
19 // Find the editable input field that is closest to the top of the dialog and
20 // matches the |predicateBlock|.
21 - (NSView*)firstEditableFieldMatchingBlock:(FieldFilterBlock)predicateBlock;
25 @implementation AutofillDetailsContainer
27 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate {
28 if (self = [super init]) {
34 - (void)addSection:(autofill::DialogSection)section {
35 base::scoped_nsobject<AutofillSectionContainer> sectionContainer(
36 [[AutofillSectionContainer alloc] initWithDelegate:delegate_
38 [sectionContainer setValidationDelegate:self];
39 [details_ addObject:sectionContainer];
43 details_.reset([[NSMutableArray alloc] init]);
45 [self addSection:autofill::SECTION_CC];
46 [self addSection:autofill::SECTION_BILLING];
47 [self addSection:autofill::SECTION_SHIPPING];
49 scrollView_.reset([[NSScrollView alloc] initWithFrame:NSZeroRect]);
50 [scrollView_ setHasVerticalScroller:YES];
51 [scrollView_ setHasHorizontalScroller:NO];
52 [scrollView_ setBorderType:NSNoBorder];
53 [scrollView_ setAutohidesScrollers:YES];
54 [self setView:scrollView_];
56 [scrollView_ setDocumentView:[[NSView alloc] initWithFrame:NSZeroRect]];
58 for (AutofillSectionContainer* container in details_.get())
59 [[scrollView_ documentView] addSubview:[container view]];
64 - (NSSize)preferredSize {
65 NSSize size = NSZeroSize;
66 for (AutofillSectionContainer* container in details_.get()) {
67 NSSize containerSize = [container preferredSize];
68 size.height += containerSize.height;
69 size.width = std::max(containerSize.width, size.width);
74 - (void)performLayout {
75 NSRect rect = NSZeroRect;
76 for (AutofillSectionContainer* container in
77 [details_ reverseObjectEnumerator]) {
78 if (![[container view] isHidden]) {
79 [container performLayout];
80 [[container view] setFrameOrigin:NSMakePoint(0, NSMaxY(rect))];
81 rect = NSUnionRect(rect, [[container view] frame]);
85 [[scrollView_ documentView] setFrameSize:[self preferredSize]];
88 - (AutofillSectionContainer*)sectionForId:(autofill::DialogSection)section {
89 for (AutofillSectionContainer* details in details_.get()) {
90 if ([details section] == section)
96 - (void)modelChanged {
97 for (AutofillSectionContainer* details in details_.get())
98 [details modelChanged];
102 // Account for a subtle timing issue. -validate is called from the dialog's
103 // -accept. -accept then hides the dialog. If the data does not validate the
104 // dialog is then reshown, focusing on the first invalid field. This happens
105 // without running the message loop, so windowWillClose has not fired when
106 // the dialog and error bubble is reshown, leading to a missing error bubble.
107 // Resetting the anchor view here forces the bubble to show.
108 errorBubbleAnchorView_ = nil;
110 bool allValid = true;
111 for (AutofillSectionContainer* details in details_.get()) {
112 if (![[details view] isHidden])
113 allValid = [details validateFor:autofill::VALIDATE_FINAL] && allValid;
118 - (NSView*)firstInvalidField {
119 return [self firstEditableFieldMatchingBlock:
120 ^BOOL (NSView<AutofillInputField>* field) {
121 return [field invalid];
125 - (NSView*)firstVisibleField {
126 return [self firstEditableFieldMatchingBlock:
127 ^BOOL (NSView<AutofillInputField>* field) {
132 - (void)scrollToView:(NSView*)field {
133 const CGFloat bottomPadding = 5.0; // Padding below the visible field.
135 NSClipView* clipView = [scrollView_ contentView];
136 NSRect fieldRect = [field convertRect:[field bounds] toView:clipView];
138 // If the entire field is already visible, let's not scroll.
139 NSRect documentRect = [clipView documentVisibleRect];
140 documentRect = [[clipView documentView] convertRect:documentRect
142 if (NSContainsRect(documentRect, fieldRect))
145 NSPoint scrollPoint = [clipView constrainScrollPoint:
146 NSMakePoint(0, NSMinY(fieldRect) - bottomPadding)];
147 [clipView scrollToPoint:scrollPoint];
148 [scrollView_ reflectScrolledClipView:clipView];
149 [self updateErrorBubble];
152 - (void)updateErrorBubble {
153 if (!delegate_->ShouldShowErrorBubble()) {
154 [errorBubbleController_ close];
158 - (void)errorBubbleWindowWillClose:(NSNotification*)notification {
159 DCHECK_EQ([notification object], [errorBubbleController_ window]);
161 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
162 [center removeObserver:self
163 name:NSWindowWillCloseNotification
164 object:[errorBubbleController_ window]];
165 errorBubbleController_ = nil;
166 errorBubbleAnchorView_ = nil;
169 - (void)showErrorBubbleForField:(NSControl<AutofillInputField>*)field {
170 // If there is already a bubble controller handling this field, reuse.
171 if (errorBubbleController_ && errorBubbleAnchorView_ == field) {
172 [errorBubbleController_ setMessage:[field validityMessage]];
177 if (errorBubbleController_)
178 [errorBubbleController_ close];
179 DCHECK(!errorBubbleController_);
180 NSWindow* parentWindow = [field window];
181 DCHECK(parentWindow);
182 errorBubbleController_ =
183 [[AutofillBubbleController alloc]
184 initWithParentWindow:parentWindow
185 message:[field validityMessage]];
187 // Handle bubble self-deleting.
188 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
189 [center addObserver:self
190 selector:@selector(errorBubbleWindowWillClose:)
191 name:NSWindowWillCloseNotification
192 object:[errorBubbleController_ window]];
194 // Compute anchor point (in window coords - views might be flipped).
195 NSRect viewRect = [field convertRect:[field bounds] toView:nil];
197 // If a bubble at maximum size with a left-aligned edge would exceed the
198 // window width, align the right edge of bubble and view. In all other
199 // cases, align the left edge of the bubble and the view.
200 // Alignment is based on maximum width to avoid the arrow changing positions
201 // if the validation bubble stays on the same field but gets a message of
202 // differing length. (E.g. "Field is required"/"Invalid Zip Code. Please
203 // check and try again" if an empty zip field gets changed to a bad zip).
205 if ((NSMinX(viewRect) + [errorBubbleController_ maxWidth]) >
206 NSWidth([parentWindow frame])) {
207 anchorPoint = NSMakePoint(NSMaxX(viewRect), NSMinY(viewRect));
208 [[errorBubbleController_ bubble] setArrowLocation:info_bubble::kTopRight];
209 [[errorBubbleController_ bubble] setAlignment:
210 info_bubble::kAlignRightEdgeToAnchorEdge];
213 anchorPoint = NSMakePoint(NSMinX(viewRect), NSMinY(viewRect));
214 [[errorBubbleController_ bubble] setArrowLocation:info_bubble::kTopLeft];
215 [[errorBubbleController_ bubble] setAlignment:
216 info_bubble::kAlignLeftEdgeToAnchorEdge];
218 [errorBubbleController_ setAnchorPoint:
219 [parentWindow convertBaseToScreen:anchorPoint]];
221 errorBubbleAnchorView_ = field;
222 [errorBubbleController_ showWindow:self];
225 - (void)hideErrorBubble {
226 [errorBubbleController_ close];
229 - (void)updateMessageForField:(NSControl<AutofillInputField>*)field {
230 // Ignore fields that are not first responder. Testing this is a bit
231 // convoluted, since for NSTextFields with firstResponder status, the
232 // firstResponder is a subview of the NSTextField, not the field itself.
233 NSView* firstResponderView =
234 base::mac::ObjCCast<NSView>([[field window] firstResponder]);
235 if (![firstResponderView isDescendantOf:field])
237 if (!delegate_->ShouldShowErrorBubble()) {
238 DCHECK(!errorBubbleController_);
242 if ([field invalid]) {
243 [self showErrorBubbleForField:field];
245 [errorBubbleController_ close];
249 - (NSView*)firstEditableFieldMatchingBlock:(FieldFilterBlock)predicateBlock {
250 base::scoped_nsobject<NSMutableArray> fields([[NSMutableArray alloc] init]);
252 for (AutofillSectionContainer* details in details_.get()) {
253 if (![[details view] isHidden])
254 [details addInputsToArray:fields];
257 NSPoint selectedFieldOrigin = NSZeroPoint;
258 NSView* selectedField = nil;
259 for (NSControl<AutofillInputField>* field in fields.get()) {
260 if (!base::mac::ObjCCast<NSControl>(field))
262 if (![field conformsToProtocol:@protocol(AutofillInputField)])
264 if ([field isHiddenOrHasHiddenAncestor])
266 if (![field isEnabled])
268 if (![field canBecomeKeyView])
270 if (!predicateBlock(field))
273 NSPoint fieldOrigin = [field convertPoint:[field bounds].origin toView:nil];
274 if (fieldOrigin.y < selectedFieldOrigin.y)
276 if (fieldOrigin.y == selectedFieldOrigin.y &&
277 fieldOrigin.x > selectedFieldOrigin.x) {
281 selectedField = field;
282 selectedFieldOrigin = fieldOrigin;
285 return selectedField;