Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / autofill / autofill_details_container.mm
blobaf7d072a4d766836ecd9dbaca44a556cd4e574e0
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"
7 #include <algorithm>
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;
23 @end
25 @implementation AutofillDetailsContainer
27 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate {
28   if (self = [super init]) {
29     delegate_ = delegate;
30   }
31   return self;
34 - (void)addSection:(autofill::DialogSection)section {
35   base::scoped_nsobject<AutofillSectionContainer> sectionContainer(
36       [[AutofillSectionContainer alloc] initWithDelegate:delegate_
37                                                 forSection:section]);
38   [sectionContainer setValidationDelegate:self];
39   [details_ addObject:sectionContainer];
42 - (void)loadView {
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]];
61   [self performLayout];
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);
70   }
71   return size;
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]);
82     }
83   }
85   [[scrollView_ documentView] setFrameSize:[self preferredSize]];
88 - (AutofillSectionContainer*)sectionForId:(autofill::DialogSection)section {
89   for (AutofillSectionContainer* details in details_.get()) {
90     if ([details section] == section)
91       return details;
92   }
93   return nil;
96 - (void)modelChanged {
97   for (AutofillSectionContainer* details in details_.get())
98     [details modelChanged];
101 - (BOOL)validate {
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;
114   }
115   return allValid;
118 - (NSView*)firstInvalidField {
119   return [self firstEditableFieldMatchingBlock:
120       ^BOOL (NSView<AutofillInputField>* field) {
121           return [field invalid];
122       }];
125 - (NSView*)firstVisibleField {
126   return [self firstEditableFieldMatchingBlock:
127       ^BOOL (NSView<AutofillInputField>* field) {
128           return YES;
129       }];
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
141                                                toView:clipView];
142   if (NSContainsRect(documentRect, fieldRect))
143     return;
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];
155   }
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]];
174     return;
175   }
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).
204   NSPoint anchorPoint;
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];
212   } else {
213     anchorPoint = NSMakePoint(NSMinX(viewRect), NSMinY(viewRect));
214     [[errorBubbleController_ bubble] setArrowLocation:info_bubble::kTopLeft];
215     [[errorBubbleController_ bubble] setAlignment:
216         info_bubble::kAlignLeftEdgeToAnchorEdge];
217   }
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])
236     return;
237   if (!delegate_->ShouldShowErrorBubble()) {
238     DCHECK(!errorBubbleController_);
239     return;
240   }
242   if ([field invalid]) {
243     [self showErrorBubbleForField:field];
244   } else {
245     [errorBubbleController_ close];
246   }
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];
255   }
257   NSPoint selectedFieldOrigin = NSZeroPoint;
258   NSView* selectedField = nil;
259   for (NSControl<AutofillInputField>* field in fields.get()) {
260     if (!base::mac::ObjCCast<NSControl>(field))
261       continue;
262     if (![field conformsToProtocol:@protocol(AutofillInputField)])
263       continue;
264     if ([field isHiddenOrHasHiddenAncestor])
265       continue;
266     if (![field isEnabled])
267       continue;
268     if (![field canBecomeKeyView])
269       continue;
270     if (!predicateBlock(field))
271       continue;
273     NSPoint fieldOrigin = [field convertPoint:[field bounds].origin toView:nil];
274     if (fieldOrigin.y < selectedFieldOrigin.y)
275       continue;
276     if (fieldOrigin.y == selectedFieldOrigin.y &&
277         fieldOrigin.x > selectedFieldOrigin.x) {
278       continue;
279     }
281     selectedField = field;
282     selectedFieldOrigin = fieldOrigin;
283   }
285   return selectedField;
288 @end