| 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 |