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