blob: 7086c60da5ffd3820ddaa0d1c1df7b1c4ff25727 [file] [log] [blame]
// Copyright 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_dialog_window_controller.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h"
#include "chrome/browser/ui/cocoa/autofill/autofill_dialog_cocoa.h"
#include "chrome/browser/ui/cocoa/autofill/autofill_dialog_constants.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_header.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_input_field.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_loading_shield_controller.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_main_container.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_overlay_controller.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_section_container.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_sign_in_container.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_textfield.h"
#import "chrome/browser/ui/cocoa/constrained_window/constrained_window_custom_window.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_view.h"
#include "grit/generated_resources.h"
#import "ui/base/cocoa/flipped_view.h"
#include "ui/base/cocoa/window_size_constants.h"
#include "ui/base/l10n/l10n_util.h"
// The minimum useful height of the contents area of the dialog.
const CGFloat kMinimumContentsHeight = 101;
#pragma mark AutofillDialogWindow
// Window class for the AutofillDialog. Its main purpose is the proper handling
// of layout requests - i.e. ensuring that layout is fully done before any
// updates of the display happen.
@interface AutofillDialogWindow : ConstrainedWindowCustomWindow {
@private
BOOL needsLayout_; // Indicates that the subviews need to be laid out.
}
// Request a new layout for all subviews. Layout occurs right before -display
// or -displayIfNeeded are invoked.
- (void)requestRelayout;
// Layout the window's subviews. Delegates to the controller.
- (void)performLayout;
@end
@implementation AutofillDialogWindow
- (void)requestRelayout {
needsLayout_ = YES;
// Ensure displayIfNeeded: is sent on the next pass through the event loop.
[self setViewsNeedDisplay:YES];
}
- (void)performLayout {
if (needsLayout_) {
needsLayout_ = NO;
AutofillDialogWindowController* controller =
base::mac::ObjCCastStrict<AutofillDialogWindowController>(
[self windowController]);
[controller performLayout];
}
}
- (void)display {
[self performLayout];
[super display];
}
- (void)displayIfNeeded {
[self performLayout];
[super displayIfNeeded];
}
@end
#pragma mark Field Editor
@interface AutofillDialogFieldEditor : NSTextView
@end
@implementation AutofillDialogFieldEditor
- (void)mouseDown:(NSEvent*)event {
// Delegate _must_ be notified before mouseDown is complete, since it needs
// to distinguish between mouseDown for already focused fields, and fields
// that will receive focus as part of the mouseDown.
AutofillTextField* textfield =
base::mac::ObjCCastStrict<AutofillTextField>([self delegate]);
[textfield onEditorMouseDown:self];
[super mouseDown:event];
}
// Intercept key down messages and forward them to the text fields delegate.
// This needs to happen in the field editor, since it handles all keyDown
// messages for NSTextField.
- (void)keyDown:(NSEvent*)event {
AutofillTextField* textfield =
base::mac::ObjCCastStrict<AutofillTextField>([self delegate]);
if ([[textfield inputDelegate] keyEvent:event
forInput:textfield] != kKeyEventHandled) {
[super keyDown:event];
}
}
@end
#pragma mark Window Controller
@interface AutofillDialogWindowController ()
// Compute maximum allowed height for the dialog.
- (CGFloat)maxHeight;
// Update size constraints on sign-in container.
- (void)updateSignInSizeConstraints;
// Notification that the WebContent's view frame has changed.
- (void)onContentViewFrameDidChange:(NSNotification*)notification;
// Update whether or not the main container is hidden.
- (void)updateMainContainerVisibility;
- (AutofillDialogWindow*)autofillWindow;
@end
@implementation AutofillDialogWindowController (NSWindowDelegate)
- (id)windowWillReturnFieldEditor:(NSWindow*)window toObject:(id)client {
AutofillTextField* textfield = base::mac::ObjCCast<AutofillTextField>(client);
if (!textfield)
return nil;
if (!fieldEditor_) {
fieldEditor_.reset([[AutofillDialogFieldEditor alloc] init]);
[fieldEditor_ setFieldEditor:YES];
}
return fieldEditor_.get();
}
@end
@implementation AutofillDialogWindowController
- (id)initWithWebContents:(content::WebContents*)webContents
dialog:(autofill::AutofillDialogCocoa*)dialog {
DCHECK(webContents);
base::scoped_nsobject<ConstrainedWindowCustomWindow> window(
[[AutofillDialogWindow alloc]
initWithContentRect:ui::kWindowSizeDeterminedLater]);
if ((self = [super initWithWindow:window])) {
[window setDelegate:self];
webContents_ = webContents;
dialog_ = dialog;
header_.reset([[AutofillHeader alloc] initWithDelegate:dialog->delegate()]);
mainContainer_.reset([[AutofillMainContainer alloc]
initWithDelegate:dialog->delegate()]);
[mainContainer_ setTarget:self];
signInContainer_.reset(
[[AutofillSignInContainer alloc] initWithDialog:dialog]);
[[signInContainer_ view] setHidden:YES];
loadingShieldController_.reset(
[[AutofillLoadingShieldController alloc] initWithDelegate:
dialog->delegate()]);
[[loadingShieldController_ view] setHidden:YES];
overlayController_.reset(
[[AutofillOverlayController alloc] initWithDelegate:
dialog->delegate()]);
[[overlayController_ view] setHidden:YES];
// This needs a flipped content view because otherwise the size
// animation looks odd. However, replacing the contentView for constrained
// windows does not work - it does custom rendering.
base::scoped_nsobject<NSView> flippedContentView(
[[FlippedView alloc] initWithFrame:
[[[self window] contentView] frame]]);
[flippedContentView setSubviews:
@[[header_ view],
[mainContainer_ view],
[signInContainer_ view],
[loadingShieldController_ view],
[overlayController_ view]]];
[flippedContentView setAutoresizingMask:
(NSViewWidthSizable | NSViewHeightSizable)];
[[[self window] contentView] addSubview:flippedContentView];
[mainContainer_ setAnchorView:[header_ anchorView]];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
- (CGFloat)maxHeight {
NSRect dialogFrameRect = [[self window] frame];
NSRect browserFrameRect =
[webContents_->GetView()->GetTopLevelNativeWindow() frame];
dialogFrameRect.size.height =
NSMaxY(dialogFrameRect) - NSMinY(browserFrameRect);
dialogFrameRect = [[self window] contentRectForFrameRect:dialogFrameRect];
return NSHeight(dialogFrameRect);
}
- (void)updateSignInSizeConstraints {
// For the minimum height, account for the size of the footer. Even though the
// footer will not be visible when the sign-in view is showing, this prevents
// the dialog's size from bouncing around.
CGFloat width = NSWidth([[[self window] contentView] frame]);
CGFloat minHeight =
kMinimumContentsHeight +
[mainContainer_ decorationSizeForWidth:width].height;
// For the maximum size, factor in the size of the header.
CGFloat headerHeight = [[header_ view] frame].size.height;
CGFloat maxHeight = std::max([self maxHeight] - headerHeight, minHeight);
[signInContainer_ constrainSizeToMinimum:NSMakeSize(width, minHeight)
maximum:NSMakeSize(width, maxHeight)];
}
- (void)onContentViewFrameDidChange:(NSNotification*)notification {
[self updateSignInSizeConstraints];
if ([[signInContainer_ view] isHidden])
[self requestRelayout];
}
- (void)updateMainContainerVisibility {
BOOL visible =
[[loadingShieldController_ view] isHidden] &&
[[overlayController_ view] isHidden] &&
[[signInContainer_ view] isHidden];
BOOL wasVisible = ![[mainContainer_ view] isHidden];
[[mainContainer_ view] setHidden:!visible];
// Postpone [mainContainer_ didBecomeVisible] until layout is complete.
if (visible && !wasVisible) {
mainContainerBecameVisible_ = YES;
[self requestRelayout];
}
}
- (AutofillDialogWindow*)autofillWindow {
return base::mac::ObjCCastStrict<AutofillDialogWindow>([self window]);
}
- (void)requestRelayout {
[[self autofillWindow] requestRelayout];
}
- (NSSize)preferredSize {
NSSize size;
if (![[overlayController_ view] isHidden]) {
// Overlay never changes window width.
size.width = NSWidth([[[self window] contentView] frame]);
size.height = [overlayController_ heightForWidth:size.width];
} else {
// Overall size is determined by either main container or sign in view.
if ([[signInContainer_ view] isHidden])
size = [mainContainer_ preferredSize];
else
size = [signInContainer_ preferredSize];
// Always make room for the header.
CGFloat headerHeight = [header_ heightForWidth:size.width];
size.height += headerHeight;
// For the minimum height, account for both the header and the footer. Even
// though the footer will not be visible when the sign-in view is showing,
// this prevents the dialog's size from bouncing around.
CGFloat minHeight = kMinimumContentsHeight;
minHeight += [mainContainer_ decorationSizeForWidth:size.width].height;
minHeight += headerHeight;
// Show as much of the main view as is possible without going past the
// bottom of the browser window, unless this would cause the dialog to be
// less tall than the minimum height.
size.height = std::min(size.height, [self maxHeight]);
size.height = std::max(size.height, minHeight);
}
return size;
}
- (void)performLayout {
NSRect contentRect = NSZeroRect;
contentRect.size = [self preferredSize];
CGFloat headerHeight = [header_ heightForWidth:NSWidth(contentRect)];
NSRect headerRect, mainRect;
NSDivideRect(contentRect, &headerRect, &mainRect, headerHeight, NSMinYEdge);
[[header_ view] setFrame:headerRect];
[header_ performLayout];
if ([[signInContainer_ view] isHidden]) {
[[mainContainer_ view] setFrame:mainRect];
[mainContainer_ performLayout];
} else {
[[signInContainer_ view] setFrame:mainRect];
}
[[loadingShieldController_ view] setFrame:contentRect];
[loadingShieldController_ performLayout];
[[overlayController_ view] setFrame:contentRect];
[overlayController_ performLayout];
NSRect frameRect = [[self window] frameRectForContentRect:contentRect];
[[self window] setFrame:frameRect display:YES];
[[self window] recalculateKeyViewLoop];
if (mainContainerBecameVisible_) {
[mainContainer_ scrollInitialEditorIntoViewAndMakeFirstResponder];
mainContainerBecameVisible_ = NO;
}
}
- (IBAction)accept:(id)sender {
if ([mainContainer_ validate])
dialog_->delegate()->OnAccept();
else
[mainContainer_ makeFirstInvalidInputFirstResponder];
}
- (IBAction)cancel:(id)sender {
dialog_->delegate()->OnCancel();
dialog_->PerformClose();
}
- (void)show {
// Resizing the browser causes the ConstrainedWindow to move.
// Observe that to allow resizes based on browser size.
// NOTE: This MUST come last after all initial setup is done, because there
// is an immediate notification post registration.
DCHECK([self window]);
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(onContentViewFrameDidChange:)
name:NSWindowDidMoveNotification
object:[self window]];
[self updateAccountChooser];
[self updateNotificationArea];
[self requestRelayout];
}
- (void)hide {
dialog_->delegate()->OnCancel();
dialog_->PerformClose();
}
- (void)updateNotificationArea {
[mainContainer_ updateNotificationArea];
}
- (void)updateAccountChooser {
[header_ update];
[mainContainer_ updateLegalDocuments];
[loadingShieldController_ update];
[self updateMainContainerVisibility];
}
- (void)updateButtonStrip {
// For the duration of the overlay, hide the main contents and the header.
// This prevents the currently focused text field "shining through". No need
// to remember previous state, because the overlay view is always the last
// state of the dialog.
[overlayController_ updateState];
[[header_ view] setHidden:![[overlayController_ view] isHidden]];
[self updateMainContainerVisibility];
}
- (void)updateSection:(autofill::DialogSection)section {
[[mainContainer_ sectionForId:section] update];
[mainContainer_ updateSaveInChrome];
}
- (void)fillSection:(autofill::DialogSection)section
forInput:(const autofill::DetailInput&)input {
[[mainContainer_ sectionForId:section] fillForInput:input];
[mainContainer_ updateSaveInChrome];
}
- (void)updateForErrors {
[mainContainer_ validate];
}
- (content::NavigationController*)showSignIn {
[self updateSignInSizeConstraints];
// Ensure |signInContainer_| is set to the same size as |mainContainer_|, to
// force its minimum size so that there will not be a resize until the
// contents are loaded.
[[signInContainer_ view] setFrameSize:[[mainContainer_ view] frame].size];
[signInContainer_ loadSignInPage];
[[signInContainer_ view] setHidden:NO];
[self updateMainContainerVisibility];
[self requestRelayout];
return [signInContainer_ navigationController];
}
- (void)getInputs:(autofill::FieldValueMap*)output
forSection:(autofill::DialogSection)section {
[[mainContainer_ sectionForId:section] getInputs:output];
}
- (NSString*)getCvc {
autofill::DialogSection section = autofill::SECTION_CC;
NSString* value = [[mainContainer_ sectionForId:section] suggestionText];
if (!value) {
section = autofill::SECTION_CC_BILLING;
value = [[mainContainer_ sectionForId:section] suggestionText];
}
return value;
}
- (BOOL)saveDetailsLocally {
return [mainContainer_ saveDetailsLocally];
}
- (void)hideSignIn {
[[signInContainer_ view] setHidden:YES];
[self updateMainContainerVisibility];
[self requestRelayout];
}
- (void)modelChanged {
[mainContainer_ modelChanged];
}
- (void)updateErrorBubble {
[mainContainer_ updateErrorBubble];
}
- (void)onSignInResize:(NSSize)size {
[signInContainer_ setPreferredSize:size];
[self requestRelayout];
}
@end
@implementation AutofillDialogWindowController (TestableAutofillDialogView)
- (void)setTextContents:(NSString*)text
forInput:(const autofill::DetailInput&)input {
for (size_t i = autofill::SECTION_MIN; i <= autofill::SECTION_MAX; ++i) {
autofill::DialogSection section = static_cast<autofill::DialogSection>(i);
// TODO(groby): Need to find the section for an input directly - wasteful.
[[mainContainer_ sectionForId:section] setFieldValue:text forInput:input];
}
}
- (void)setTextContents:(NSString*)text
ofSuggestionForSection:(autofill::DialogSection)section {
[[mainContainer_ sectionForId:section] setSuggestionFieldValue:text];
}
- (void)activateFieldForInput:(const autofill::DetailInput&)input {
for (size_t i = autofill::SECTION_MIN; i <= autofill::SECTION_MAX; ++i) {
autofill::DialogSection section = static_cast<autofill::DialogSection>(i);
[[mainContainer_ sectionForId:section] activateFieldForInput:input];
}
}
- (content::WebContents*)getSignInWebContents {
return [signInContainer_ webContents];
}
- (BOOL)isShowingOverlay {
return ![[overlayController_ view] isHidden];
}
@end