Home | History | Annotate | Download | only in autofill
      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.
      4 
      5 #import "chrome/browser/ui/cocoa/autofill/autofill_dialog_window_controller.h"
      6 
      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 "grit/generated_resources.h"
     24 #import "ui/base/cocoa/flipped_view.h"
     25 #include "ui/base/cocoa/window_size_constants.h"
     26 #include "ui/base/l10n/l10n_util.h"
     27 
     28 // The minimum useful height of the contents area of the dialog.
     29 const CGFloat kMinimumContentsHeight = 101;
     30 
     31 #pragma mark AutofillDialogWindow
     32 
     33 // Window class for the AutofillDialog. Its main purpose is the proper handling
     34 // of layout requests - i.e. ensuring that layout is fully done before any
     35 // updates of the display happen.
     36 @interface AutofillDialogWindow : ConstrainedWindowCustomWindow {
     37  @private
     38   BOOL needsLayout_;  // Indicates that the subviews need to be laid out.
     39 }
     40 
     41 // Request a new layout for all subviews. Layout occurs right before -display
     42 // or -displayIfNeeded are invoked.
     43 - (void)requestRelayout;
     44 
     45 // Layout the window's subviews. Delegates to the controller.
     46 - (void)performLayout;
     47 
     48 @end
     49 
     50 
     51 @implementation AutofillDialogWindow
     52 
     53 - (void)requestRelayout {
     54   needsLayout_ = YES;
     55 
     56   // Ensure displayIfNeeded: is sent on the next pass through the event loop.
     57   [self setViewsNeedDisplay:YES];
     58 }
     59 
     60 - (void)performLayout {
     61   if (needsLayout_) {
     62     needsLayout_ = NO;
     63     AutofillDialogWindowController* controller =
     64         base::mac::ObjCCastStrict<AutofillDialogWindowController>(
     65             [self windowController]);
     66     [controller performLayout];
     67   }
     68 }
     69 
     70 - (void)display {
     71   [self performLayout];
     72   [super display];
     73 }
     74 
     75 - (void)displayIfNeeded {
     76   [self performLayout];
     77   [super displayIfNeeded];
     78 }
     79 
     80 @end
     81 
     82 #pragma mark Field Editor
     83 
     84 @interface AutofillDialogFieldEditor : NSTextView
     85 @end
     86 
     87 
     88 @implementation AutofillDialogFieldEditor
     89 
     90 - (void)mouseDown:(NSEvent*)event {
     91   // Delegate _must_ be notified before mouseDown is complete, since it needs
     92   // to distinguish between mouseDown for already focused fields, and fields
     93   // that will receive focus as part of the mouseDown.
     94   AutofillTextField* textfield =
     95       base::mac::ObjCCastStrict<AutofillTextField>([self delegate]);
     96   [textfield onEditorMouseDown:self];
     97   [super mouseDown:event];
     98 }
     99 
    100 // Intercept key down messages and forward them to the text fields delegate.
    101 // This needs to happen in the field editor, since it handles all keyDown
    102 // messages for NSTextField.
    103 - (void)keyDown:(NSEvent*)event {
    104   AutofillTextField* textfield =
    105       base::mac::ObjCCastStrict<AutofillTextField>([self delegate]);
    106   if ([[textfield inputDelegate] keyEvent:event
    107                                  forInput:textfield] != kKeyEventHandled) {
    108     [super keyDown:event];
    109   }
    110 }
    111 
    112 @end
    113 
    114 
    115 #pragma mark Window Controller
    116 
    117 @interface AutofillDialogWindowController ()
    118 
    119 // Compute maximum allowed height for the dialog.
    120 - (CGFloat)maxHeight;
    121 
    122 // Update size constraints on sign-in container.
    123 - (void)updateSignInSizeConstraints;
    124 
    125 // Notification that the WebContent's view frame has changed.
    126 - (void)onContentViewFrameDidChange:(NSNotification*)notification;
    127 
    128 // Update whether or not the main container is hidden.
    129 - (void)updateMainContainerVisibility;
    130 
    131 - (AutofillDialogWindow*)autofillWindow;
    132 
    133 @end
    134 
    135 
    136 @implementation AutofillDialogWindowController (NSWindowDelegate)
    137 
    138 - (id)windowWillReturnFieldEditor:(NSWindow*)window toObject:(id)client {
    139   AutofillTextField* textfield = base::mac::ObjCCast<AutofillTextField>(client);
    140   if (!textfield)
    141     return nil;
    142 
    143   if (!fieldEditor_) {
    144     fieldEditor_.reset([[AutofillDialogFieldEditor alloc] init]);
    145     [fieldEditor_ setFieldEditor:YES];
    146   }
    147   return fieldEditor_.get();
    148 }
    149 
    150 @end
    151 
    152 
    153 @implementation AutofillDialogWindowController
    154 
    155 - (id)initWithWebContents:(content::WebContents*)webContents
    156                    dialog:(autofill::AutofillDialogCocoa*)dialog {
    157   DCHECK(webContents);
    158 
    159   base::scoped_nsobject<ConstrainedWindowCustomWindow> window(
    160       [[AutofillDialogWindow alloc]
    161           initWithContentRect:ui::kWindowSizeDeterminedLater]);
    162 
    163   if ((self = [super initWithWindow:window])) {
    164     [window setDelegate:self];
    165     webContents_ = webContents;
    166     dialog_ = dialog;
    167 
    168     header_.reset([[AutofillHeader alloc] initWithDelegate:dialog->delegate()]);
    169 
    170     mainContainer_.reset([[AutofillMainContainer alloc]
    171                              initWithDelegate:dialog->delegate()]);
    172     [mainContainer_ setTarget:self];
    173 
    174     signInContainer_.reset(
    175         [[AutofillSignInContainer alloc] initWithDialog:dialog]);
    176     [[signInContainer_ view] setHidden:YES];
    177 
    178     loadingShieldController_.reset(
    179         [[AutofillLoadingShieldController alloc] initWithDelegate:
    180             dialog->delegate()]);
    181     [[loadingShieldController_ view] setHidden:YES];
    182 
    183     overlayController_.reset(
    184         [[AutofillOverlayController alloc] initWithDelegate:
    185             dialog->delegate()]);
    186     [[overlayController_ view] setHidden:YES];
    187 
    188     // This needs a flipped content view because otherwise the size
    189     // animation looks odd. However, replacing the contentView for constrained
    190     // windows does not work - it does custom rendering.
    191     base::scoped_nsobject<NSView> flippedContentView(
    192         [[FlippedView alloc] initWithFrame:
    193             [[[self window] contentView] frame]]);
    194     [flippedContentView setSubviews:
    195         @[[header_ view],
    196           [mainContainer_ view],
    197           [signInContainer_ view],
    198           [loadingShieldController_ view],
    199           [overlayController_ view]]];
    200     [flippedContentView setAutoresizingMask:
    201         (NSViewWidthSizable | NSViewHeightSizable)];
    202     [[[self window] contentView] addSubview:flippedContentView];
    203     [mainContainer_ setAnchorView:[header_ anchorView]];
    204   }
    205   return self;
    206 }
    207 
    208 - (void)dealloc {
    209   [[NSNotificationCenter defaultCenter] removeObserver:self];
    210   [super dealloc];
    211 }
    212 
    213 - (CGFloat)maxHeight {
    214   NSRect dialogFrameRect = [[self window] frame];
    215   NSRect browserFrameRect = [webContents_->GetTopLevelNativeWindow() frame];
    216   dialogFrameRect.size.height =
    217       NSMaxY(dialogFrameRect) - NSMinY(browserFrameRect);
    218   dialogFrameRect = [[self window] contentRectForFrameRect:dialogFrameRect];
    219   return NSHeight(dialogFrameRect);
    220 }
    221 
    222 - (void)updateSignInSizeConstraints {
    223   // For the minimum height, account for the size of the footer. Even though the
    224   // footer will not be visible when the sign-in view is showing, this prevents
    225   // the dialog's size from bouncing around.
    226   CGFloat width = NSWidth([[[self window] contentView] frame]);
    227   CGFloat minHeight =
    228       kMinimumContentsHeight +
    229       [mainContainer_ decorationSizeForWidth:width].height;
    230 
    231   // For the maximum size, factor in the size of the header.
    232   CGFloat headerHeight = [[header_ view] frame].size.height;
    233   CGFloat maxHeight = std::max([self maxHeight] - headerHeight, minHeight);
    234 
    235   [signInContainer_ constrainSizeToMinimum:NSMakeSize(width, minHeight)
    236                                    maximum:NSMakeSize(width, maxHeight)];
    237 }
    238 
    239 - (void)onContentViewFrameDidChange:(NSNotification*)notification {
    240   [self updateSignInSizeConstraints];
    241   if ([[signInContainer_ view] isHidden])
    242     [self requestRelayout];
    243 }
    244 
    245 - (void)updateMainContainerVisibility {
    246   BOOL visible =
    247       [[loadingShieldController_ view] isHidden] &&
    248       [[overlayController_ view] isHidden] &&
    249       [[signInContainer_ view] isHidden];
    250   BOOL wasVisible = ![[mainContainer_ view] isHidden];
    251   [[mainContainer_ view] setHidden:!visible];
    252 
    253   // Postpone [mainContainer_ didBecomeVisible] until layout is complete.
    254   if (visible && !wasVisible) {
    255     mainContainerBecameVisible_ = YES;
    256     [self requestRelayout];
    257   }
    258 }
    259 
    260 - (AutofillDialogWindow*)autofillWindow {
    261   return base::mac::ObjCCastStrict<AutofillDialogWindow>([self window]);
    262 }
    263 
    264 - (void)requestRelayout {
    265   [[self autofillWindow] requestRelayout];
    266 }
    267 
    268 - (NSSize)preferredSize {
    269   NSSize size;
    270 
    271   if (![[overlayController_ view] isHidden]) {
    272     // Overlay never changes window width.
    273     size.width = NSWidth([[[self window] contentView] frame]);
    274     size.height = [overlayController_ heightForWidth:size.width];
    275   } else {
    276     // Overall size is determined by either main container or sign in view.
    277     if ([[signInContainer_ view] isHidden])
    278       size = [mainContainer_ preferredSize];
    279     else
    280       size = [signInContainer_ preferredSize];
    281 
    282     // Always make room for the header.
    283     CGFloat headerHeight = [header_ preferredSize].height;
    284     size.height += headerHeight;
    285 
    286     // For the minimum height, account for both the header and the footer. Even
    287     // though the footer will not be visible when the sign-in view is showing,
    288     // this prevents the dialog's size from bouncing around.
    289     CGFloat minHeight = kMinimumContentsHeight;
    290     minHeight += [mainContainer_ decorationSizeForWidth:size.width].height;
    291     minHeight += headerHeight;
    292 
    293     // Show as much of the main view as is possible without going past the
    294     // bottom of the browser window, unless this would cause the dialog to be
    295     // less tall than the minimum height.
    296     size.height = std::min(size.height, [self maxHeight]);
    297     size.height = std::max(size.height, minHeight);
    298   }
    299 
    300   return size;
    301 }
    302 
    303 - (void)performLayout {
    304   NSRect contentRect = NSZeroRect;
    305   contentRect.size = [self preferredSize];
    306 
    307   CGFloat headerHeight = [header_ preferredSize].height;
    308   NSRect headerRect, mainRect;
    309   NSDivideRect(contentRect, &headerRect, &mainRect, headerHeight, NSMinYEdge);
    310 
    311   [[header_ view] setFrame:headerRect];
    312   [header_ performLayout];
    313 
    314   if ([[signInContainer_ view] isHidden]) {
    315     [[mainContainer_ view] setFrame:mainRect];
    316     [mainContainer_ performLayout];
    317   } else {
    318     [[signInContainer_ view] setFrame:mainRect];
    319   }
    320 
    321   [[loadingShieldController_ view] setFrame:contentRect];
    322   [loadingShieldController_ performLayout];
    323 
    324   [[overlayController_ view] setFrame:contentRect];
    325   [overlayController_ performLayout];
    326 
    327   NSRect frameRect = [[self window] frameRectForContentRect:contentRect];
    328   [[self window] setFrame:frameRect display:YES];
    329 
    330   [[self window] recalculateKeyViewLoop];
    331 
    332   if (mainContainerBecameVisible_) {
    333     [mainContainer_ scrollInitialEditorIntoViewAndMakeFirstResponder];
    334     mainContainerBecameVisible_ = NO;
    335   }
    336 }
    337 
    338 - (IBAction)accept:(id)sender {
    339   if ([mainContainer_ validate])
    340     dialog_->delegate()->OnAccept();
    341   else
    342     [mainContainer_ makeFirstInvalidInputFirstResponder];
    343 }
    344 
    345 - (IBAction)cancel:(id)sender {
    346   dialog_->delegate()->OnCancel();
    347   dialog_->PerformClose();
    348 }
    349 
    350 - (void)show {
    351   // Resizing the browser causes the ConstrainedWindow to move.
    352   // Observe that to allow resizes based on browser size.
    353   // NOTE: This MUST come last after all initial setup is done, because there
    354   // is an immediate notification post registration.
    355   DCHECK([self window]);
    356   [[NSNotificationCenter defaultCenter]
    357       addObserver:self
    358          selector:@selector(onContentViewFrameDidChange:)
    359              name:NSWindowDidMoveNotification
    360            object:[self window]];
    361 
    362   [self updateAccountChooser];
    363   [self updateNotificationArea];
    364   [self requestRelayout];
    365 }
    366 
    367 - (void)hide {
    368   dialog_->delegate()->OnCancel();
    369   dialog_->PerformClose();
    370 }
    371 
    372 - (void)updateNotificationArea {
    373   [mainContainer_ updateNotificationArea];
    374 }
    375 
    376 - (void)updateAccountChooser {
    377   [header_ update];
    378   [mainContainer_ updateLegalDocuments];
    379   [loadingShieldController_ update];
    380   [self updateMainContainerVisibility];
    381 }
    382 
    383 - (void)updateButtonStrip {
    384   // For the duration of the overlay, hide the main contents and the header.
    385   // This prevents the currently focused text field "shining through". No need
    386   // to remember previous state, because the overlay view is always the last
    387   // state of the dialog.
    388   [overlayController_ updateState];
    389   [[header_ view] setHidden:![[overlayController_ view] isHidden]];
    390   [self updateMainContainerVisibility];
    391 }
    392 
    393 - (void)updateSection:(autofill::DialogSection)section {
    394   [[mainContainer_ sectionForId:section] update];
    395   [mainContainer_ updateSaveInChrome];
    396 }
    397 
    398 - (void)fillSection:(autofill::DialogSection)section
    399             forType:(autofill::ServerFieldType)type {
    400   [[mainContainer_ sectionForId:section] fillForType:type];
    401   [mainContainer_ updateSaveInChrome];
    402 }
    403 
    404 - (void)updateForErrors {
    405   [mainContainer_ validate];
    406 }
    407 
    408 - (content::NavigationController*)showSignIn {
    409   [self updateSignInSizeConstraints];
    410   // Ensure |signInContainer_| is set to the same size as |mainContainer_|, to
    411   // force its minimum size so that there will not be a resize until the
    412   // contents are loaded.
    413   [[signInContainer_ view] setFrameSize:[[mainContainer_ view] frame].size];
    414   [signInContainer_ loadSignInPage];
    415 
    416   [[signInContainer_ view] setHidden:NO];
    417   [self updateMainContainerVisibility];
    418   [self requestRelayout];
    419 
    420   return [signInContainer_ navigationController];
    421 }
    422 
    423 - (void)getInputs:(autofill::FieldValueMap*)output
    424        forSection:(autofill::DialogSection)section {
    425   [[mainContainer_ sectionForId:section] getInputs:output];
    426 }
    427 
    428 - (NSString*)getCvc {
    429   autofill::DialogSection section = autofill::SECTION_CC;
    430   NSString* value = [[mainContainer_ sectionForId:section] suggestionText];
    431   if (!value) {
    432     section = autofill::SECTION_CC_BILLING;
    433     value = [[mainContainer_ sectionForId:section] suggestionText];
    434   }
    435   return value;
    436 }
    437 
    438 - (BOOL)saveDetailsLocally {
    439   return [mainContainer_ saveDetailsLocally];
    440 }
    441 
    442 - (void)hideSignIn {
    443   [[signInContainer_ view] setHidden:YES];
    444   [self updateMainContainerVisibility];
    445   [self requestRelayout];
    446 }
    447 
    448 - (void)modelChanged {
    449   [mainContainer_ modelChanged];
    450 }
    451 
    452 - (void)updateErrorBubble {
    453   [mainContainer_ updateErrorBubble];
    454 }
    455 
    456 - (void)onSignInResize:(NSSize)size {
    457   [signInContainer_ setPreferredSize:size];
    458   [self requestRelayout];
    459 }
    460 
    461 - (void)validateSection:(autofill::DialogSection)section {
    462   [[mainContainer_ sectionForId:section] validateFor:autofill::VALIDATE_EDIT];
    463 }
    464 
    465 @end
    466