Upstream version 5.34.92.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / autofill / autofill_details_container.mm
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
15 typedef BOOL (^FieldFilterBlock)(NSView<AutofillInputField>*);
16
17 @interface AutofillDetailsContainer ()
18
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;
22
23 @end
24
25 @implementation AutofillDetailsContainer
26
27 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate {
28   if (self = [super init]) {
29     delegate_ = delegate;
30   }
31   return self;
32 }
33
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];
40 }
41
42 - (void)loadView {
43   details_.reset([[NSMutableArray alloc] init]);
44
45   [self addSection:autofill::SECTION_CC];
46   [self addSection:autofill::SECTION_BILLING];
47   [self addSection:autofill::SECTION_CC_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(NSMinX(fieldRect), 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:
220       [parentWindow convertBaseToScreen: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