OLD | NEW |
| (Empty) |
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_details_container.h" | |
6 | |
7 #include <algorithm> | |
8 | |
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" | |
14 #include "ui/base/cocoa/cocoa_base_utils.h" | |
15 | |
16 typedef BOOL (^FieldFilterBlock)(NSView<AutofillInputField>*); | |
17 | |
18 @interface AutofillDetailsContainer () | |
19 | |
20 // Find the editable input field that is closest to the top of the dialog and | |
21 // matches the |predicateBlock|. | |
22 - (NSView*)firstEditableFieldMatchingBlock:(FieldFilterBlock)predicateBlock; | |
23 | |
24 @end | |
25 | |
26 @implementation AutofillDetailsContainer | |
27 | |
28 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate { | |
29 if (self = [super init]) { | |
30 delegate_ = delegate; | |
31 } | |
32 return self; | |
33 } | |
34 | |
35 - (void)addSection:(autofill::DialogSection)section { | |
36 base::scoped_nsobject<AutofillSectionContainer> sectionContainer( | |
37 [[AutofillSectionContainer alloc] initWithDelegate:delegate_ | |
38 forSection:section]); | |
39 [sectionContainer setValidationDelegate:self]; | |
40 [details_ addObject:sectionContainer]; | |
41 } | |
42 | |
43 - (void)loadView { | |
44 details_.reset([[NSMutableArray alloc] init]); | |
45 | |
46 [self addSection:autofill::SECTION_CC]; | |
47 [self addSection:autofill::SECTION_BILLING]; | |
48 [self addSection:autofill::SECTION_SHIPPING]; | |
49 | |
50 scrollView_.reset([[NSScrollView alloc] initWithFrame:NSZeroRect]); | |
51 [scrollView_ setHasVerticalScroller:YES]; | |
52 [scrollView_ setHasHorizontalScroller:NO]; | |
53 [scrollView_ setBorderType:NSNoBorder]; | |
54 [scrollView_ setAutohidesScrollers:YES]; | |
55 [self setView:scrollView_]; | |
56 | |
57 [scrollView_ setDocumentView:[[NSView alloc] initWithFrame:NSZeroRect]]; | |
58 | |
59 for (AutofillSectionContainer* container in details_.get()) | |
60 [[scrollView_ documentView] addSubview:[container view]]; | |
61 | |
62 [self performLayout]; | |
63 } | |
64 | |
65 - (NSSize)preferredSize { | |
66 NSSize size = NSZeroSize; | |
67 for (AutofillSectionContainer* container in details_.get()) { | |
68 NSSize containerSize = [container preferredSize]; | |
69 size.height += containerSize.height; | |
70 size.width = std::max(containerSize.width, size.width); | |
71 } | |
72 return size; | |
73 } | |
74 | |
75 - (void)performLayout { | |
76 NSRect rect = NSZeroRect; | |
77 for (AutofillSectionContainer* container in | |
78 [details_ reverseObjectEnumerator]) { | |
79 if (![[container view] isHidden]) { | |
80 [container performLayout]; | |
81 [[container view] setFrameOrigin:NSMakePoint(0, NSMaxY(rect))]; | |
82 rect = NSUnionRect(rect, [[container view] frame]); | |
83 } | |
84 } | |
85 | |
86 [[scrollView_ documentView] setFrameSize:[self preferredSize]]; | |
87 } | |
88 | |
89 - (AutofillSectionContainer*)sectionForId:(autofill::DialogSection)section { | |
90 for (AutofillSectionContainer* details in details_.get()) { | |
91 if ([details section] == section) | |
92 return details; | |
93 } | |
94 return nil; | |
95 } | |
96 | |
97 - (void)modelChanged { | |
98 for (AutofillSectionContainer* details in details_.get()) | |
99 [details modelChanged]; | |
100 } | |
101 | |
102 - (BOOL)validate { | |
103 // Account for a subtle timing issue. -validate is called from the dialog's | |
104 // -accept. -accept then hides the dialog. If the data does not validate the | |
105 // dialog is then reshown, focusing on the first invalid field. This happens | |
106 // without running the message loop, so windowWillClose has not fired when | |
107 // the dialog and error bubble is reshown, leading to a missing error bubble. | |
108 // Resetting the anchor view here forces the bubble to show. | |
109 errorBubbleAnchorView_ = nil; | |
110 | |
111 bool allValid = true; | |
112 for (AutofillSectionContainer* details in details_.get()) { | |
113 if (![[details view] isHidden]) | |
114 allValid = [details validateFor:autofill::VALIDATE_FINAL] && allValid; | |
115 } | |
116 return allValid; | |
117 } | |
118 | |
119 - (NSView*)firstInvalidField { | |
120 return [self firstEditableFieldMatchingBlock: | |
121 ^BOOL (NSView<AutofillInputField>* field) { | |
122 return [field invalid]; | |
123 }]; | |
124 } | |
125 | |
126 - (NSView*)firstVisibleField { | |
127 return [self firstEditableFieldMatchingBlock: | |
128 ^BOOL (NSView<AutofillInputField>* field) { | |
129 return YES; | |
130 }]; | |
131 } | |
132 | |
133 - (void)scrollToView:(NSView*)field { | |
134 const CGFloat bottomPadding = 5.0; // Padding below the visible field. | |
135 | |
136 NSClipView* clipView = [scrollView_ contentView]; | |
137 NSRect fieldRect = [field convertRect:[field bounds] toView:clipView]; | |
138 | |
139 // If the entire field is already visible, let's not scroll. | |
140 NSRect documentRect = [clipView documentVisibleRect]; | |
141 documentRect = [[clipView documentView] convertRect:documentRect | |
142 toView:clipView]; | |
143 if (NSContainsRect(documentRect, fieldRect)) | |
144 return; | |
145 | |
146 NSPoint scrollPoint = [clipView constrainScrollPoint: | |
147 NSMakePoint(0, NSMinY(fieldRect) - bottomPadding)]; | |
148 [clipView scrollToPoint:scrollPoint]; | |
149 [scrollView_ reflectScrolledClipView:clipView]; | |
150 [self updateErrorBubble]; | |
151 } | |
152 | |
153 - (void)updateErrorBubble { | |
154 if (!delegate_->ShouldShowErrorBubble()) { | |
155 [errorBubbleController_ close]; | |
156 } | |
157 } | |
158 | |
159 - (void)errorBubbleWindowWillClose:(NSNotification*)notification { | |
160 DCHECK_EQ([notification object], [errorBubbleController_ window]); | |
161 | |
162 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; | |
163 [center removeObserver:self | |
164 name:NSWindowWillCloseNotification | |
165 object:[errorBubbleController_ window]]; | |
166 errorBubbleController_ = nil; | |
167 errorBubbleAnchorView_ = nil; | |
168 } | |
169 | |
170 - (void)showErrorBubbleForField:(NSControl<AutofillInputField>*)field { | |
171 // If there is already a bubble controller handling this field, reuse. | |
172 if (errorBubbleController_ && errorBubbleAnchorView_ == field) { | |
173 [errorBubbleController_ setMessage:[field validityMessage]]; | |
174 | |
175 return; | |
176 } | |
177 | |
178 if (errorBubbleController_) | |
179 [errorBubbleController_ close]; | |
180 DCHECK(!errorBubbleController_); | |
181 NSWindow* parentWindow = [field window]; | |
182 DCHECK(parentWindow); | |
183 errorBubbleController_ = | |
184 [[AutofillBubbleController alloc] | |
185 initWithParentWindow:parentWindow | |
186 message:[field validityMessage]]; | |
187 | |
188 // Handle bubble self-deleting. | |
189 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; | |
190 [center addObserver:self | |
191 selector:@selector(errorBubbleWindowWillClose:) | |
192 name:NSWindowWillCloseNotification | |
193 object:[errorBubbleController_ window]]; | |
194 | |
195 // Compute anchor point (in window coords - views might be flipped). | |
196 NSRect viewRect = [field convertRect:[field bounds] toView:nil]; | |
197 | |
198 // If a bubble at maximum size with a left-aligned edge would exceed the | |
199 // window width, align the right edge of bubble and view. In all other | |
200 // cases, align the left edge of the bubble and the view. | |
201 // Alignment is based on maximum width to avoid the arrow changing positions | |
202 // if the validation bubble stays on the same field but gets a message of | |
203 // differing length. (E.g. "Field is required"/"Invalid Zip Code. Please | |
204 // check and try again" if an empty zip field gets changed to a bad zip). | |
205 NSPoint anchorPoint; | |
206 if ((NSMinX(viewRect) + [errorBubbleController_ maxWidth]) > | |
207 NSWidth([parentWindow frame])) { | |
208 anchorPoint = NSMakePoint(NSMaxX(viewRect), NSMinY(viewRect)); | |
209 [[errorBubbleController_ bubble] setArrowLocation:info_bubble::kTopRight]; | |
210 [[errorBubbleController_ bubble] setAlignment: | |
211 info_bubble::kAlignRightEdgeToAnchorEdge]; | |
212 | |
213 } else { | |
214 anchorPoint = NSMakePoint(NSMinX(viewRect), NSMinY(viewRect)); | |
215 [[errorBubbleController_ bubble] setArrowLocation:info_bubble::kTopLeft]; | |
216 [[errorBubbleController_ bubble] setAlignment: | |
217 info_bubble::kAlignLeftEdgeToAnchorEdge]; | |
218 } | |
219 [errorBubbleController_ setAnchorPoint:ui::ConvertPointFromWindowToScreen( | |
220 parentWindow, anchorPoint)]; | |
221 | |
222 errorBubbleAnchorView_ = field; | |
223 [errorBubbleController_ showWindow:self]; | |
224 } | |
225 | |
226 - (void)hideErrorBubble { | |
227 [errorBubbleController_ close]; | |
228 } | |
229 | |
230 - (void)updateMessageForField:(NSControl<AutofillInputField>*)field { | |
231 // Ignore fields that are not first responder. Testing this is a bit | |
232 // convoluted, since for NSTextFields with firstResponder status, the | |
233 // firstResponder is a subview of the NSTextField, not the field itself. | |
234 NSView* firstResponderView = | |
235 base::mac::ObjCCast<NSView>([[field window] firstResponder]); | |
236 if (![firstResponderView isDescendantOf:field]) | |
237 return; | |
238 if (!delegate_->ShouldShowErrorBubble()) { | |
239 DCHECK(!errorBubbleController_); | |
240 return; | |
241 } | |
242 | |
243 if ([field invalid]) { | |
244 [self showErrorBubbleForField:field]; | |
245 } else { | |
246 [errorBubbleController_ close]; | |
247 } | |
248 } | |
249 | |
250 - (NSView*)firstEditableFieldMatchingBlock:(FieldFilterBlock)predicateBlock { | |
251 base::scoped_nsobject<NSMutableArray> fields([[NSMutableArray alloc] init]); | |
252 | |
253 for (AutofillSectionContainer* details in details_.get()) { | |
254 if (![[details view] isHidden]) | |
255 [details addInputsToArray:fields]; | |
256 } | |
257 | |
258 NSPoint selectedFieldOrigin = NSZeroPoint; | |
259 NSView* selectedField = nil; | |
260 for (NSControl<AutofillInputField>* field in fields.get()) { | |
261 if (!base::mac::ObjCCast<NSControl>(field)) | |
262 continue; | |
263 if (![field conformsToProtocol:@protocol(AutofillInputField)]) | |
264 continue; | |
265 if ([field isHiddenOrHasHiddenAncestor]) | |
266 continue; | |
267 if (![field isEnabled]) | |
268 continue; | |
269 if (![field canBecomeKeyView]) | |
270 continue; | |
271 if (!predicateBlock(field)) | |
272 continue; | |
273 | |
274 NSPoint fieldOrigin = [field convertPoint:[field bounds].origin toView:nil]; | |
275 if (fieldOrigin.y < selectedFieldOrigin.y) | |
276 continue; | |
277 if (fieldOrigin.y == selectedFieldOrigin.y && | |
278 fieldOrigin.x > selectedFieldOrigin.x) { | |
279 continue; | |
280 } | |
281 | |
282 selectedField = field; | |
283 selectedFieldOrigin = fieldOrigin; | |
284 } | |
285 | |
286 return selectedField; | |
287 } | |
288 | |
289 @end | |
OLD | NEW |