1 // Copyright 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_dialog_window_controller.h"
7 #include "base/mac/foundation_util.h"
8 #include "base/mac/scoped_nsobject.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h"
11 #include "chrome/browser/ui/cocoa/autofill/autofill_dialog_cocoa.h"
12 #include "chrome/browser/ui/cocoa/autofill/autofill_dialog_constants.h"
13 #import "chrome/browser/ui/cocoa/autofill/autofill_header.h"
14 #import "chrome/browser/ui/cocoa/autofill/autofill_input_field.h"
15 #import "chrome/browser/ui/cocoa/autofill/autofill_loading_shield_controller.h"
16 #import "chrome/browser/ui/cocoa/autofill/autofill_main_container.h"
17 #import "chrome/browser/ui/cocoa/autofill/autofill_overlay_controller.h"
18 #import "chrome/browser/ui/cocoa/autofill/autofill_section_container.h"
19 #import "chrome/browser/ui/cocoa/autofill/autofill_sign_in_container.h"
20 #import "chrome/browser/ui/cocoa/autofill/autofill_textfield.h"
21 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_custom_window.h"
22 #include "content/public/browser/web_contents.h"
23 #import "ui/base/cocoa/flipped_view.h"
24 #include "ui/base/cocoa/window_size_constants.h"
26 // The minimum useful height of the contents area of the dialog.
27 const CGFloat kMinimumContentsHeight = 101;
29 #pragma mark AutofillDialogWindow
31 // Window class for the AutofillDialog. Its main purpose is the proper handling
32 // of layout requests - i.e. ensuring that layout is fully done before any
33 // updates of the display happen.
34 @interface AutofillDialogWindow : ConstrainedWindowCustomWindow {
36 BOOL needsLayout_; // Indicates that the subviews need to be laid out.
39 // Request a new layout for all subviews. Layout occurs right before -display
40 // or -displayIfNeeded are invoked.
41 - (void)requestRelayout;
43 // Layout the window's subviews. Delegates to the controller.
44 - (void)performLayout;
49 @implementation AutofillDialogWindow
51 - (void)requestRelayout {
54 // Ensure displayIfNeeded: is sent on the next pass through the event loop.
55 [self setViewsNeedDisplay:YES];
58 - (void)performLayout {
61 AutofillDialogWindowController* controller =
62 base::mac::ObjCCastStrict<AutofillDialogWindowController>(
63 [self windowController]);
64 [controller performLayout];
73 - (void)displayIfNeeded {
75 [super displayIfNeeded];
80 #pragma mark Field Editor
82 @interface AutofillDialogFieldEditor : NSTextView
86 @implementation AutofillDialogFieldEditor
88 - (void)mouseDown:(NSEvent*)event {
89 // Delegate _must_ be notified before mouseDown is complete, since it needs
90 // to distinguish between mouseDown for already focused fields, and fields
91 // that will receive focus as part of the mouseDown.
92 AutofillTextField* textfield =
93 base::mac::ObjCCastStrict<AutofillTextField>([self delegate]);
94 [textfield onEditorMouseDown:self];
95 [super mouseDown:event];
98 // Intercept key down messages and forward them to the text fields delegate.
99 // This needs to happen in the field editor, since it handles all keyDown
100 // messages for NSTextField.
101 - (void)keyDown:(NSEvent*)event {
102 AutofillTextField* textfield =
103 base::mac::ObjCCastStrict<AutofillTextField>([self delegate]);
104 if ([[textfield inputDelegate] keyEvent:event
105 forInput:textfield] != kKeyEventHandled) {
106 [super keyDown:event];
113 #pragma mark Window Controller
115 @interface AutofillDialogWindowController ()
117 // Compute maximum allowed height for the dialog.
118 - (CGFloat)maxHeight;
120 // Update size constraints on sign-in container.
121 - (void)updateSignInSizeConstraints;
123 // Notification that the WebContent's view frame has changed.
124 - (void)onContentViewFrameDidChange:(NSNotification*)notification;
126 // Update whether or not the main container is hidden.
127 - (void)updateMainContainerVisibility;
129 - (AutofillDialogWindow*)autofillWindow;
134 @implementation AutofillDialogWindowController (NSWindowDelegate)
136 - (id)windowWillReturnFieldEditor:(NSWindow*)window toObject:(id)client {
137 AutofillTextField* textfield = base::mac::ObjCCast<AutofillTextField>(client);
142 fieldEditor_.reset([[AutofillDialogFieldEditor alloc] init]);
143 [fieldEditor_ setFieldEditor:YES];
145 return fieldEditor_.get();
151 @implementation AutofillDialogWindowController
153 - (id)initWithWebContents:(content::WebContents*)webContents
154 dialog:(autofill::AutofillDialogCocoa*)dialog {
157 base::scoped_nsobject<ConstrainedWindowCustomWindow> window(
158 [[AutofillDialogWindow alloc]
159 initWithContentRect:ui::kWindowSizeDeterminedLater]);
161 if ((self = [super initWithWindow:window])) {
162 [window setDelegate:self];
163 webContents_ = webContents;
166 header_.reset([[AutofillHeader alloc] initWithDelegate:dialog->delegate()]);
168 mainContainer_.reset([[AutofillMainContainer alloc]
169 initWithDelegate:dialog->delegate()]);
170 [mainContainer_ setTarget:self];
172 signInContainer_.reset(
173 [[AutofillSignInContainer alloc] initWithDialog:dialog]);
174 [[signInContainer_ view] setHidden:YES];
176 loadingShieldController_.reset(
177 [[AutofillLoadingShieldController alloc] initWithDelegate:
178 dialog->delegate()]);
179 [[loadingShieldController_ view] setHidden:YES];
181 overlayController_.reset(
182 [[AutofillOverlayController alloc] initWithDelegate:
183 dialog->delegate()]);
184 [[overlayController_ view] setHidden:YES];
186 // This needs a flipped content view because otherwise the size
187 // animation looks odd. However, replacing the contentView for constrained
188 // windows does not work - it does custom rendering.
189 base::scoped_nsobject<NSView> flippedContentView(
190 [[FlippedView alloc] initWithFrame:
191 [[[self window] contentView] frame]]);
192 [flippedContentView setSubviews:
194 [mainContainer_ view],
195 [signInContainer_ view],
196 [loadingShieldController_ view],
197 [overlayController_ view]]];
198 [flippedContentView setAutoresizingMask:
199 (NSViewWidthSizable | NSViewHeightSizable)];
200 [[[self window] contentView] addSubview:flippedContentView];
201 [mainContainer_ setAnchorView:[header_ anchorView]];
207 [[NSNotificationCenter defaultCenter] removeObserver:self];
211 - (CGFloat)maxHeight {
212 NSRect dialogFrameRect = [[self window] frame];
213 NSRect browserFrameRect = [webContents_->GetTopLevelNativeWindow() frame];
214 dialogFrameRect.size.height =
215 NSMaxY(dialogFrameRect) - NSMinY(browserFrameRect);
216 dialogFrameRect = [[self window] contentRectForFrameRect:dialogFrameRect];
217 return NSHeight(dialogFrameRect);
220 - (void)updateSignInSizeConstraints {
221 // For the minimum height, account for the size of the footer. Even though the
222 // footer will not be visible when the sign-in view is showing, this prevents
223 // the dialog's size from bouncing around.
224 CGFloat width = NSWidth([[[self window] contentView] frame]);
226 kMinimumContentsHeight +
227 [mainContainer_ decorationSizeForWidth:width].height;
229 // For the maximum size, factor in the size of the header.
230 CGFloat headerHeight = [[header_ view] frame].size.height;
231 CGFloat maxHeight = std::max([self maxHeight] - headerHeight, minHeight);
233 [signInContainer_ constrainSizeToMinimum:NSMakeSize(width, minHeight)
234 maximum:NSMakeSize(width, maxHeight)];
237 - (void)onContentViewFrameDidChange:(NSNotification*)notification {
238 [self updateSignInSizeConstraints];
239 if ([[signInContainer_ view] isHidden])
240 [self requestRelayout];
243 - (void)updateMainContainerVisibility {
245 [[loadingShieldController_ view] isHidden] &&
246 [[overlayController_ view] isHidden] &&
247 [[signInContainer_ view] isHidden];
248 BOOL wasVisible = ![[mainContainer_ view] isHidden];
249 [[mainContainer_ view] setHidden:!visible];
251 // Postpone [mainContainer_ didBecomeVisible] until layout is complete.
252 if (visible && !wasVisible) {
253 mainContainerBecameVisible_ = YES;
254 [self requestRelayout];
258 - (AutofillDialogWindow*)autofillWindow {
259 return base::mac::ObjCCastStrict<AutofillDialogWindow>([self window]);
262 - (void)requestRelayout {
263 [[self autofillWindow] requestRelayout];
266 - (NSSize)preferredSize {
269 if (![[overlayController_ view] isHidden]) {
270 // Overlay never changes window width.
271 size.width = NSWidth([[[self window] contentView] frame]);
272 size.height = [overlayController_ heightForWidth:size.width];
274 // Overall size is determined by either main container or sign in view.
275 if ([[signInContainer_ view] isHidden])
276 size = [mainContainer_ preferredSize];
278 size = [signInContainer_ preferredSize];
280 // Always make room for the header.
281 CGFloat headerHeight = [header_ preferredSize].height;
282 size.height += headerHeight;
284 // For the minimum height, account for both the header and the footer. Even
285 // though the footer will not be visible when the sign-in view is showing,
286 // this prevents the dialog's size from bouncing around.
287 CGFloat minHeight = kMinimumContentsHeight;
288 minHeight += [mainContainer_ decorationSizeForWidth:size.width].height;
289 minHeight += headerHeight;
291 // Show as much of the main view as is possible without going past the
292 // bottom of the browser window, unless this would cause the dialog to be
293 // less tall than the minimum height.
294 size.height = std::min(size.height, [self maxHeight]);
295 size.height = std::max(size.height, minHeight);
301 - (void)performLayout {
302 NSRect contentRect = NSZeroRect;
303 contentRect.size = [self preferredSize];
305 CGFloat headerHeight = [header_ preferredSize].height;
306 NSRect headerRect, mainRect;
307 NSDivideRect(contentRect, &headerRect, &mainRect, headerHeight, NSMinYEdge);
309 [[header_ view] setFrame:headerRect];
310 [header_ performLayout];
312 if ([[signInContainer_ view] isHidden]) {
313 [[mainContainer_ view] setFrame:mainRect];
314 [mainContainer_ performLayout];
316 [[signInContainer_ view] setFrame:mainRect];
319 [[loadingShieldController_ view] setFrame:contentRect];
320 [loadingShieldController_ performLayout];
322 [[overlayController_ view] setFrame:contentRect];
323 [overlayController_ performLayout];
325 NSRect frameRect = [[self window] frameRectForContentRect:contentRect];
326 [[self window] setFrame:frameRect display:YES];
328 [[self window] recalculateKeyViewLoop];
330 if (mainContainerBecameVisible_) {
331 [mainContainer_ scrollInitialEditorIntoViewAndMakeFirstResponder];
332 mainContainerBecameVisible_ = NO;
336 - (IBAction)accept:(id)sender {
337 if ([mainContainer_ validate])
338 dialog_->delegate()->OnAccept();
340 [mainContainer_ makeFirstInvalidInputFirstResponder];
343 - (IBAction)cancel:(id)sender {
344 dialog_->delegate()->OnCancel();
345 dialog_->PerformClose();
349 // Resizing the browser causes the ConstrainedWindow to move.
350 // Observe that to allow resizes based on browser size.
351 // NOTE: This MUST come last after all initial setup is done, because there
352 // is an immediate notification post registration.
353 DCHECK([self window]);
354 [[NSNotificationCenter defaultCenter]
356 selector:@selector(onContentViewFrameDidChange:)
357 name:NSWindowDidMoveNotification
358 object:[self window]];
360 [self updateAccountChooser];
361 [self updateNotificationArea];
362 [self requestRelayout];
366 dialog_->delegate()->OnCancel();
367 dialog_->PerformClose();
370 - (void)updateNotificationArea {
371 [mainContainer_ updateNotificationArea];
374 - (void)updateAccountChooser {
376 [mainContainer_ updateLegalDocuments];
377 [loadingShieldController_ update];
378 [self updateMainContainerVisibility];
381 - (void)updateButtonStrip {
382 // For the duration of the overlay, hide the main contents and the header.
383 // This prevents the currently focused text field "shining through". No need
384 // to remember previous state, because the overlay view is always the last
385 // state of the dialog.
386 [overlayController_ updateState];
387 [[header_ view] setHidden:![[overlayController_ view] isHidden]];
388 [self updateMainContainerVisibility];
391 - (void)updateSection:(autofill::DialogSection)section {
392 [[mainContainer_ sectionForId:section] update];
393 [mainContainer_ updateSaveInChrome];
396 - (void)fillSection:(autofill::DialogSection)section
397 forType:(autofill::ServerFieldType)type {
398 [[mainContainer_ sectionForId:section] fillForType:type];
399 [mainContainer_ updateSaveInChrome];
402 - (void)updateForErrors {
403 [mainContainer_ validate];
406 - (content::NavigationController*)showSignIn:(const GURL&)url {
407 [self updateSignInSizeConstraints];
408 // Ensure |signInContainer_| is set to the same size as |mainContainer_|, to
409 // force its minimum size so that there will not be a resize until the
410 // contents are loaded.
411 [[signInContainer_ view] setFrameSize:[[mainContainer_ view] frame].size];
412 [signInContainer_ loadSignInPage:url];
414 [[signInContainer_ view] setHidden:NO];
415 [self updateMainContainerVisibility];
416 [self requestRelayout];
418 return [signInContainer_ navigationController];
421 - (void)getInputs:(autofill::FieldValueMap*)output
422 forSection:(autofill::DialogSection)section {
423 [[mainContainer_ sectionForId:section] getInputs:output];
426 - (NSString*)getCvc {
427 autofill::DialogSection section = autofill::SECTION_CC;
428 NSString* value = [[mainContainer_ sectionForId:section] suggestionText];
430 section = autofill::SECTION_CC_BILLING;
431 value = [[mainContainer_ sectionForId:section] suggestionText];
436 - (BOOL)saveDetailsLocally {
437 return [mainContainer_ saveDetailsLocally];
441 [[signInContainer_ view] setHidden:YES];
442 [self updateMainContainerVisibility];
443 [self requestRelayout];
446 - (void)modelChanged {
447 [mainContainer_ modelChanged];
450 - (void)updateErrorBubble {
451 [mainContainer_ updateErrorBubble];
454 - (void)onSignInResize:(NSSize)size {
455 [signInContainer_ setPreferredSize:size];
456 [self requestRelayout];
459 - (void)validateSection:(autofill::DialogSection)section {
460 [[mainContainer_ sectionForId:section] validateFor:autofill::VALIDATE_EDIT];