| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/autofill/autofill_details_container.h" |
| |
| #include <algorithm> |
| |
| #include "base/mac/foundation_util.h" |
| #include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h" |
| #import "chrome/browser/ui/cocoa/autofill/autofill_bubble_controller.h" |
| #import "chrome/browser/ui/cocoa/autofill/autofill_section_container.h" |
| #import "chrome/browser/ui/cocoa/info_bubble_view.h" |
| |
| typedef BOOL (^FieldFilterBlock)(NSView<AutofillInputField>*); |
| |
| @interface AutofillDetailsContainer () |
| |
| // Find the editable input field that is closest to the top of the dialog and |
| // matches the |predicateBlock|. |
| - (NSView*)firstEditableFieldMatchingBlock:(FieldFilterBlock)predicateBlock; |
| |
| @end |
| |
| @implementation AutofillDetailsContainer |
| |
| - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate { |
| if (self = [super init]) { |
| delegate_ = delegate; |
| } |
| return self; |
| } |
| |
| - (void)addSection:(autofill::DialogSection)section { |
| base::scoped_nsobject<AutofillSectionContainer> sectionContainer( |
| [[AutofillSectionContainer alloc] initWithDelegate:delegate_ |
| forSection:section]); |
| [sectionContainer setValidationDelegate:self]; |
| [details_ addObject:sectionContainer]; |
| } |
| |
| - (void)loadView { |
| details_.reset([[NSMutableArray alloc] init]); |
| |
| [self addSection:autofill::SECTION_CC]; |
| [self addSection:autofill::SECTION_BILLING]; |
| [self addSection:autofill::SECTION_CC_BILLING]; |
| [self addSection:autofill::SECTION_SHIPPING]; |
| |
| scrollView_.reset([[NSScrollView alloc] initWithFrame:NSZeroRect]); |
| [scrollView_ setHasVerticalScroller:YES]; |
| [scrollView_ setHasHorizontalScroller:NO]; |
| [scrollView_ setBorderType:NSNoBorder]; |
| [scrollView_ setAutohidesScrollers:YES]; |
| [self setView:scrollView_]; |
| |
| [scrollView_ setDocumentView:[[NSView alloc] initWithFrame:NSZeroRect]]; |
| |
| for (AutofillSectionContainer* container in details_.get()) |
| [[scrollView_ documentView] addSubview:[container view]]; |
| |
| [self performLayout]; |
| } |
| |
| - (NSSize)preferredSize { |
| NSSize size = NSZeroSize; |
| for (AutofillSectionContainer* container in details_.get()) { |
| NSSize containerSize = [container preferredSize]; |
| size.height += containerSize.height; |
| size.width = std::max(containerSize.width, size.width); |
| } |
| return size; |
| } |
| |
| - (void)performLayout { |
| NSRect rect = NSZeroRect; |
| for (AutofillSectionContainer* container in |
| [details_ reverseObjectEnumerator]) { |
| if (![[container view] isHidden]) { |
| [container performLayout]; |
| [[container view] setFrameOrigin:NSMakePoint(0, NSMaxY(rect))]; |
| rect = NSUnionRect(rect, [[container view] frame]); |
| } |
| } |
| |
| [[scrollView_ documentView] setFrameSize:[self preferredSize]]; |
| } |
| |
| - (AutofillSectionContainer*)sectionForId:(autofill::DialogSection)section { |
| for (AutofillSectionContainer* details in details_.get()) { |
| if ([details section] == section) |
| return details; |
| } |
| return nil; |
| } |
| |
| - (void)modelChanged { |
| for (AutofillSectionContainer* details in details_.get()) |
| [details modelChanged]; |
| } |
| |
| - (BOOL)validate { |
| bool allValid = true; |
| for (AutofillSectionContainer* details in details_.get()) { |
| if (![[details view] isHidden]) |
| allValid = [details validateFor:autofill::VALIDATE_FINAL] && allValid; |
| } |
| return allValid; |
| } |
| |
| - (NSView*)firstInvalidField { |
| return [self firstEditableFieldMatchingBlock: |
| ^BOOL (NSView<AutofillInputField>* field) { |
| return [field invalid]; |
| }]; |
| } |
| |
| - (NSView*)firstVisibleField { |
| return [self firstEditableFieldMatchingBlock: |
| ^BOOL (NSView<AutofillInputField>* field) { |
| return YES; |
| }]; |
| } |
| |
| - (void)scrollToView:(NSView*)field { |
| const CGFloat bottomPadding = 5.0; // Padding below the visible field. |
| |
| NSClipView* clipView = [scrollView_ contentView]; |
| NSRect fieldRect = [field convertRect:[field bounds] toView:clipView]; |
| |
| // If the entire field is already visible, let's not scroll. |
| NSRect documentRect = [clipView documentVisibleRect]; |
| documentRect = [[clipView documentView] convertRect:documentRect |
| toView:clipView]; |
| if (NSContainsRect(documentRect, fieldRect)) |
| return; |
| |
| NSPoint scrollPoint = [clipView constrainScrollPoint: |
| NSMakePoint(NSMinX(fieldRect), NSMinY(fieldRect) - bottomPadding)]; |
| [clipView scrollToPoint:scrollPoint]; |
| [scrollView_ reflectScrolledClipView:clipView]; |
| [self updateErrorBubble]; |
| } |
| |
| - (void)updateErrorBubble { |
| if (!delegate_->ShouldShowErrorBubble()) { |
| [errorBubbleController_ close]; |
| } |
| } |
| |
| - (void)errorBubbleWindowWillClose:(NSNotification*)notification { |
| DCHECK_EQ([notification object], [errorBubbleController_ window]); |
| |
| NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
| [center removeObserver:self |
| name:NSWindowWillCloseNotification |
| object:[errorBubbleController_ window]]; |
| errorBubbleController_ = nil; |
| } |
| |
| - (void)showErrorBubbleForField:(NSControl<AutofillInputField>*)field { |
| if (errorBubbleController_) |
| [errorBubbleController_ close]; |
| DCHECK(!errorBubbleController_); |
| NSWindow* parentWindow = [field window]; |
| DCHECK(parentWindow); |
| errorBubbleController_ = |
| [[AutofillBubbleController alloc] |
| initWithParentWindow:parentWindow |
| message:[field validityMessage]]; |
| |
| // Handle bubble self-deleting. |
| NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
| [center addObserver:self |
| selector:@selector(errorBubbleWindowWillClose:) |
| name:NSWindowWillCloseNotification |
| object:[errorBubbleController_ window]]; |
| |
| // Compute anchor point (in window coords - views might be flipped). |
| NSRect viewRect = [field convertRect:[field bounds] toView:nil]; |
| |
| // If a bubble at maximum size with a left-aligned edge would exceed the |
| // window width, align the right edge of bubble and view. In all other |
| // cases, align the left edge of the bubble and the view. |
| // Alignment is based on maximum width to avoid the arrow changing positions |
| // if the validation bubble stays on the same field but gets a message of |
| // differing length. (E.g. "Field is required"/"Invalid Zip Code. Please |
| // check and try again" if an empty zip field gets changed to a bad zip). |
| NSPoint anchorPoint; |
| if ((NSMinX(viewRect) + [errorBubbleController_ maxWidth]) > |
| NSWidth([parentWindow frame])) { |
| anchorPoint = NSMakePoint(NSMaxX(viewRect), NSMinY(viewRect)); |
| [[errorBubbleController_ bubble] setArrowLocation:info_bubble::kTopRight]; |
| [[errorBubbleController_ bubble] setAlignment: |
| info_bubble::kAlignRightEdgeToAnchorEdge]; |
| |
| } else { |
| anchorPoint = NSMakePoint(NSMinX(viewRect), NSMinY(viewRect)); |
| [[errorBubbleController_ bubble] setArrowLocation:info_bubble::kTopLeft]; |
| [[errorBubbleController_ bubble] setAlignment: |
| info_bubble::kAlignLeftEdgeToAnchorEdge]; |
| } |
| [errorBubbleController_ setAnchorPoint: |
| [parentWindow convertBaseToScreen:anchorPoint]]; |
| |
| [errorBubbleController_ showWindow:self]; |
| } |
| |
| - (void)hideErrorBubble { |
| [errorBubble_ setHidden:YES]; |
| } |
| |
| - (void)updateMessageForField:(NSControl<AutofillInputField>*)field { |
| // Ignore fields that are not first responder. Testing this is a bit |
| // convoluted, since for NSTextFields with firstResponder status, the |
| // firstResponder is a subview of the NSTextField, not the field itself. |
| NSView* firstResponderView = |
| base::mac::ObjCCast<NSView>([[field window] firstResponder]); |
| if (![firstResponderView isDescendantOf:field]) |
| return; |
| if (!delegate_->ShouldShowErrorBubble()) { |
| DCHECK(!errorBubbleController_); |
| return; |
| } |
| |
| if ([field invalid]) { |
| [self showErrorBubbleForField:field]; |
| } else { |
| [errorBubbleController_ close]; |
| } |
| } |
| |
| - (NSView*)firstEditableFieldMatchingBlock:(FieldFilterBlock)predicateBlock { |
| base::scoped_nsobject<NSMutableArray> fields([[NSMutableArray alloc] init]); |
| |
| for (AutofillSectionContainer* details in details_.get()) { |
| if (![[details view] isHidden]) |
| [details addInputsToArray:fields]; |
| } |
| |
| NSPoint selectedFieldOrigin = NSZeroPoint; |
| NSView* selectedField = nil; |
| for (NSControl<AutofillInputField>* field in fields.get()) { |
| if (!base::mac::ObjCCast<NSControl>(field)) |
| continue; |
| if (![field conformsToProtocol:@protocol(AutofillInputField)]) |
| continue; |
| if ([field isHiddenOrHasHiddenAncestor]) |
| continue; |
| if (![field isEnabled]) |
| continue; |
| if (![field canBecomeKeyView]) |
| continue; |
| if (!predicateBlock(field)) |
| continue; |
| |
| NSPoint fieldOrigin = [field convertPoint:[field bounds].origin toView:nil]; |
| if (fieldOrigin.y < selectedFieldOrigin.y) |
| continue; |
| if (fieldOrigin.y == selectedFieldOrigin.y && |
| fieldOrigin.x > selectedFieldOrigin.x) { |
| continue; |
| } |
| |
| selectedField = field; |
| selectedFieldOrigin = fieldOrigin; |
| } |
| |
| return selectedField; |
| } |
| |
| @end |