Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / autofill / autofill_dialog_window_controller.mm
blob6e66fed0a741a3b896c6fe197d883114e3b38a0e
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 #include "content/public/browser/web_contents_view.h"
24 #include "grit/generated_resources.h"
25 #import "ui/base/cocoa/flipped_view.h"
26 #include "ui/base/cocoa/window_size_constants.h"
27 #include "ui/base/l10n/l10n_util.h"
29 // The minimum useful height of the contents area of the dialog.
30 const CGFloat kMinimumContentsHeight = 101;
32 #pragma mark AutofillDialogWindow
34 // Window class for the AutofillDialog. Its main purpose is the proper handling
35 // of layout requests - i.e. ensuring that layout is fully done before any
36 // updates of the display happen.
37 @interface AutofillDialogWindow : ConstrainedWindowCustomWindow {
38  @private
39   BOOL needsLayout_;  // Indicates that the subviews need to be laid out.
42 // Request a new layout for all subviews. Layout occurs right before -display
43 // or -displayIfNeeded are invoked.
44 - (void)requestRelayout;
46 // Layout the window's subviews. Delegates to the controller.
47 - (void)performLayout;
49 @end
52 @implementation AutofillDialogWindow
54 - (void)requestRelayout {
55   needsLayout_ = YES;
57   // Ensure displayIfNeeded: is sent on the next pass through the event loop.
58   [self setViewsNeedDisplay:YES];
61 - (void)performLayout {
62   if (needsLayout_) {
63     needsLayout_ = NO;
64     AutofillDialogWindowController* controller =
65         base::mac::ObjCCastStrict<AutofillDialogWindowController>(
66             [self windowController]);
67     [controller performLayout];
68   }
71 - (void)display {
72   [self performLayout];
73   [super display];
76 - (void)displayIfNeeded {
77   [self performLayout];
78   [super displayIfNeeded];
81 @end
83 #pragma mark Field Editor
85 @interface AutofillDialogFieldEditor : NSTextView
86 @end
89 @implementation AutofillDialogFieldEditor
91 - (void)mouseDown:(NSEvent*)event {
92   // Delegate _must_ be notified before mouseDown is complete, since it needs
93   // to distinguish between mouseDown for already focused fields, and fields
94   // that will receive focus as part of the mouseDown.
95   AutofillTextField* textfield =
96       base::mac::ObjCCastStrict<AutofillTextField>([self delegate]);
97   [textfield onEditorMouseDown:self];
98   [super mouseDown:event];
101 // Intercept key down messages and forward them to the text fields delegate.
102 // This needs to happen in the field editor, since it handles all keyDown
103 // messages for NSTextField.
104 - (void)keyDown:(NSEvent*)event {
105   AutofillTextField* textfield =
106       base::mac::ObjCCastStrict<AutofillTextField>([self delegate]);
107   if ([[textfield inputDelegate] keyEvent:event
108                                  forInput:textfield] != kKeyEventHandled) {
109     [super keyDown:event];
110   }
113 @end
116 #pragma mark Window Controller
118 @interface AutofillDialogWindowController ()
120 // Compute maximum allowed height for the dialog.
121 - (CGFloat)maxHeight;
123 // Update size constraints on sign-in container.
124 - (void)updateSignInSizeConstraints;
126 // Notification that the WebContent's view frame has changed.
127 - (void)onContentViewFrameDidChange:(NSNotification*)notification;
129 // Update whether or not the main container is hidden.
130 - (void)updateMainContainerVisibility;
132 - (AutofillDialogWindow*)autofillWindow;
134 @end
137 @implementation AutofillDialogWindowController (NSWindowDelegate)
139 - (id)windowWillReturnFieldEditor:(NSWindow*)window toObject:(id)client {
140   AutofillTextField* textfield = base::mac::ObjCCast<AutofillTextField>(client);
141   if (!textfield)
142     return nil;
144   if (!fieldEditor_) {
145     fieldEditor_.reset([[AutofillDialogFieldEditor alloc] init]);
146     [fieldEditor_ setFieldEditor:YES];
147   }
148   return fieldEditor_.get();
151 @end
154 @implementation AutofillDialogWindowController
156 - (id)initWithWebContents:(content::WebContents*)webContents
157                    dialog:(autofill::AutofillDialogCocoa*)dialog {
158   DCHECK(webContents);
160   base::scoped_nsobject<ConstrainedWindowCustomWindow> window(
161       [[AutofillDialogWindow alloc]
162           initWithContentRect:ui::kWindowSizeDeterminedLater]);
164   if ((self = [super initWithWindow:window])) {
165     [window setDelegate:self];
166     webContents_ = webContents;
167     dialog_ = dialog;
169     header_.reset([[AutofillHeader alloc] initWithDelegate:dialog->delegate()]);
171     mainContainer_.reset([[AutofillMainContainer alloc]
172                              initWithDelegate:dialog->delegate()]);
173     [mainContainer_ setTarget:self];
175     signInContainer_.reset(
176         [[AutofillSignInContainer alloc] initWithDialog:dialog]);
177     [[signInContainer_ view] setHidden:YES];
179     loadingShieldController_.reset(
180         [[AutofillLoadingShieldController alloc] initWithDelegate:
181             dialog->delegate()]);
182     [[loadingShieldController_ view] setHidden:YES];
184     overlayController_.reset(
185         [[AutofillOverlayController alloc] initWithDelegate:
186             dialog->delegate()]);
187     [[overlayController_ view] setHidden:YES];
189     // This needs a flipped content view because otherwise the size
190     // animation looks odd. However, replacing the contentView for constrained
191     // windows does not work - it does custom rendering.
192     base::scoped_nsobject<NSView> flippedContentView(
193         [[FlippedView alloc] initWithFrame:
194             [[[self window] contentView] frame]]);
195     [flippedContentView setSubviews:
196         @[[header_ view],
197           [mainContainer_ view],
198           [signInContainer_ view],
199           [loadingShieldController_ view],
200           [overlayController_ view]]];
201     [flippedContentView setAutoresizingMask:
202         (NSViewWidthSizable | NSViewHeightSizable)];
203     [[[self window] contentView] addSubview:flippedContentView];
204     [mainContainer_ setAnchorView:[header_ anchorView]];
205   }
206   return self;
209 - (void)dealloc {
210   [[NSNotificationCenter defaultCenter] removeObserver:self];
211   [super dealloc];
214 - (CGFloat)maxHeight {
215   NSRect dialogFrameRect = [[self window] frame];
216   NSRect browserFrameRect =
217       [webContents_->GetView()->GetTopLevelNativeWindow() frame];
218   dialogFrameRect.size.height =
219       NSMaxY(dialogFrameRect) - NSMinY(browserFrameRect);
220   dialogFrameRect = [[self window] contentRectForFrameRect:dialogFrameRect];
221   return NSHeight(dialogFrameRect);
224 - (void)updateSignInSizeConstraints {
225   // For the minimum height, account for the size of the footer. Even though the
226   // footer will not be visible when the sign-in view is showing, this prevents
227   // the dialog's size from bouncing around.
228   CGFloat width = NSWidth([[[self window] contentView] frame]);
229   CGFloat minHeight =
230       kMinimumContentsHeight +
231       [mainContainer_ decorationSizeForWidth:width].height;
233   // For the maximum size, factor in the size of the header.
234   CGFloat headerHeight = [[header_ view] frame].size.height;
235   CGFloat maxHeight = std::max([self maxHeight] - headerHeight, minHeight);
237   [signInContainer_ constrainSizeToMinimum:NSMakeSize(width, minHeight)
238                                    maximum:NSMakeSize(width, maxHeight)];
241 - (void)onContentViewFrameDidChange:(NSNotification*)notification {
242   [self updateSignInSizeConstraints];
243   if ([[signInContainer_ view] isHidden])
244     [self requestRelayout];
247 - (void)updateMainContainerVisibility {
248   BOOL visible =
249       [[loadingShieldController_ view] isHidden] &&
250       [[overlayController_ view] isHidden] &&
251       [[signInContainer_ view] isHidden];
252   BOOL wasVisible = ![[mainContainer_ view] isHidden];
253   [[mainContainer_ view] setHidden:!visible];
255   // Postpone [mainContainer_ didBecomeVisible] until layout is complete.
256   if (visible && !wasVisible) {
257     mainContainerBecameVisible_ = YES;
258     [self requestRelayout];
259   }
262 - (AutofillDialogWindow*)autofillWindow {
263   return base::mac::ObjCCastStrict<AutofillDialogWindow>([self window]);
266 - (void)requestRelayout {
267   [[self autofillWindow] requestRelayout];
270 - (NSSize)preferredSize {
271   NSSize size;
273   if (![[overlayController_ view] isHidden]) {
274     // Overlay never changes window width.
275     size.width = NSWidth([[[self window] contentView] frame]);
276     size.height = [overlayController_ heightForWidth:size.width];
277   } else {
278     // Overall size is determined by either main container or sign in view.
279     if ([[signInContainer_ view] isHidden])
280       size = [mainContainer_ preferredSize];
281     else
282       size = [signInContainer_ preferredSize];
284     // Always make room for the header.
285     CGFloat headerHeight = [header_ preferredSize].height;
286     size.height += headerHeight;
288     // For the minimum height, account for both the header and the footer. Even
289     // though the footer will not be visible when the sign-in view is showing,
290     // this prevents the dialog's size from bouncing around.
291     CGFloat minHeight = kMinimumContentsHeight;
292     minHeight += [mainContainer_ decorationSizeForWidth:size.width].height;
293     minHeight += headerHeight;
295     // Show as much of the main view as is possible without going past the
296     // bottom of the browser window, unless this would cause the dialog to be
297     // less tall than the minimum height.
298     size.height = std::min(size.height, [self maxHeight]);
299     size.height = std::max(size.height, minHeight);
300   }
302   return size;
305 - (void)performLayout {
306   NSRect contentRect = NSZeroRect;
307   contentRect.size = [self preferredSize];
309   CGFloat headerHeight = [header_ preferredSize].height;
310   NSRect headerRect, mainRect;
311   NSDivideRect(contentRect, &headerRect, &mainRect, headerHeight, NSMinYEdge);
313   [[header_ view] setFrame:headerRect];
314   [header_ performLayout];
316   if ([[signInContainer_ view] isHidden]) {
317     [[mainContainer_ view] setFrame:mainRect];
318     [mainContainer_ performLayout];
319   } else {
320     [[signInContainer_ view] setFrame:mainRect];
321   }
323   [[loadingShieldController_ view] setFrame:contentRect];
324   [loadingShieldController_ performLayout];
326   [[overlayController_ view] setFrame:contentRect];
327   [overlayController_ performLayout];
329   NSRect frameRect = [[self window] frameRectForContentRect:contentRect];
330   [[self window] setFrame:frameRect display:YES];
332   [[self window] recalculateKeyViewLoop];
334   if (mainContainerBecameVisible_) {
335     [mainContainer_ scrollInitialEditorIntoViewAndMakeFirstResponder];
336     mainContainerBecameVisible_ = NO;
337   }
340 - (IBAction)accept:(id)sender {
341   if ([mainContainer_ validate])
342     dialog_->delegate()->OnAccept();
343   else
344     [mainContainer_ makeFirstInvalidInputFirstResponder];
347 - (IBAction)cancel:(id)sender {
348   dialog_->delegate()->OnCancel();
349   dialog_->PerformClose();
352 - (void)show {
353   // Resizing the browser causes the ConstrainedWindow to move.
354   // Observe that to allow resizes based on browser size.
355   // NOTE: This MUST come last after all initial setup is done, because there
356   // is an immediate notification post registration.
357   DCHECK([self window]);
358   [[NSNotificationCenter defaultCenter]
359       addObserver:self
360          selector:@selector(onContentViewFrameDidChange:)
361              name:NSWindowDidMoveNotification
362            object:[self window]];
364   [self updateAccountChooser];
365   [self updateNotificationArea];
366   [self requestRelayout];
369 - (void)hide {
370   dialog_->delegate()->OnCancel();
371   dialog_->PerformClose();
374 - (void)updateNotificationArea {
375   [mainContainer_ updateNotificationArea];
378 - (void)updateAccountChooser {
379   [header_ update];
380   [mainContainer_ updateLegalDocuments];
381   [loadingShieldController_ update];
382   [self updateMainContainerVisibility];
385 - (void)updateButtonStrip {
386   // For the duration of the overlay, hide the main contents and the header.
387   // This prevents the currently focused text field "shining through". No need
388   // to remember previous state, because the overlay view is always the last
389   // state of the dialog.
390   [overlayController_ updateState];
391   [[header_ view] setHidden:![[overlayController_ view] isHidden]];
392   [self updateMainContainerVisibility];
395 - (void)updateSection:(autofill::DialogSection)section {
396   [[mainContainer_ sectionForId:section] update];
397   [mainContainer_ updateSaveInChrome];
400 - (void)fillSection:(autofill::DialogSection)section
401             forType:(autofill::ServerFieldType)type {
402   [[mainContainer_ sectionForId:section] fillForType:type];
403   [mainContainer_ updateSaveInChrome];
406 - (void)updateForErrors {
407   [mainContainer_ validate];
410 - (content::NavigationController*)showSignIn {
411   [self updateSignInSizeConstraints];
412   // Ensure |signInContainer_| is set to the same size as |mainContainer_|, to
413   // force its minimum size so that there will not be a resize until the
414   // contents are loaded.
415   [[signInContainer_ view] setFrameSize:[[mainContainer_ view] frame].size];
416   [signInContainer_ loadSignInPage];
418   [[signInContainer_ view] setHidden:NO];
419   [self updateMainContainerVisibility];
420   [self requestRelayout];
422   return [signInContainer_ navigationController];
425 - (void)getInputs:(autofill::FieldValueMap*)output
426        forSection:(autofill::DialogSection)section {
427   [[mainContainer_ sectionForId:section] getInputs:output];
430 - (NSString*)getCvc {
431   autofill::DialogSection section = autofill::SECTION_CC;
432   NSString* value = [[mainContainer_ sectionForId:section] suggestionText];
433   if (!value) {
434     section = autofill::SECTION_CC_BILLING;
435     value = [[mainContainer_ sectionForId:section] suggestionText];
436   }
437   return value;
440 - (BOOL)saveDetailsLocally {
441   return [mainContainer_ saveDetailsLocally];
444 - (void)hideSignIn {
445   [[signInContainer_ view] setHidden:YES];
446   [self updateMainContainerVisibility];
447   [self requestRelayout];
450 - (void)modelChanged {
451   [mainContainer_ modelChanged];
454 - (void)updateErrorBubble {
455   [mainContainer_ updateErrorBubble];
458 - (void)onSignInResize:(NSSize)size {
459   [signInContainer_ setPreferredSize:size];
460   [self requestRelayout];
463 @end
466 @implementation AutofillDialogWindowController (TestableAutofillDialogView)
468 - (void)setTextContents:(NSString*)text
469                forType:(autofill::ServerFieldType)type {
470   for (size_t i = autofill::SECTION_MIN; i <= autofill::SECTION_MAX; ++i) {
471     autofill::DialogSection section = static_cast<autofill::DialogSection>(i);
472     // TODO(groby): Need to find the section for an input directly - wasteful.
473     [[mainContainer_ sectionForId:section] setFieldValue:text forType:type];
474   }
477 - (void)setTextContents:(NSString*)text
478  ofSuggestionForSection:(autofill::DialogSection)section {
479   [[mainContainer_ sectionForId:section] setSuggestionFieldValue:text];
482 - (void)activateFieldForType:(autofill::ServerFieldType)type {
483   for (size_t i = autofill::SECTION_MIN; i <= autofill::SECTION_MAX; ++i) {
484     autofill::DialogSection section = static_cast<autofill::DialogSection>(i);
485     [[mainContainer_ sectionForId:section] activateFieldForType:type];
486   }
489 - (content::WebContents*)getSignInWebContents {
490   return [signInContainer_ webContents];
493 - (BOOL)isShowingOverlay {
494   return ![[overlayController_ view] isHidden];
497 @end