1 // Copyright (c) 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_main_container.h"
10 #include "base/mac/foundation_util.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h"
13 #include "chrome/browser/ui/chrome_style.h"
14 #import "chrome/browser/ui/cocoa/autofill/autofill_details_container.h"
15 #include "chrome/browser/ui/cocoa/autofill/autofill_dialog_constants.h"
16 #import "chrome/browser/ui/cocoa/autofill/autofill_notification_container.h"
17 #import "chrome/browser/ui/cocoa/autofill/autofill_tooltip_controller.h"
18 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_button.h"
19 #import "chrome/browser/ui/cocoa/key_equivalent_constants.h"
20 #include "grit/theme_resources.h"
21 #include "skia/ext/skia_utils_mac.h"
22 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
23 #import "ui/base/cocoa/controls/blue_label_button.h"
24 #import "ui/base/cocoa/controls/hyperlink_text_view.h"
25 #include "ui/base/cocoa/window_size_constants.h"
26 #include "ui/gfx/range/range.h"
30 // Padding between buttons and the last suggestion or details view. The mock
31 // has a total of 30px - but 10px are already provided by details/suggestions.
32 const CGFloat kButtonVerticalPadding = 20.0;
34 // Padding around the text for the legal documents.
35 const CGFloat kLegalDocumentsPadding = 20.0;
37 // The font color for the legal documents text. Set to match the Views
39 const SkColor kLegalDocumentsTextColor = SkColorSetRGB(102, 102, 102);
43 @interface AutofillMainContainer (Private)
44 - (void)buildWindowButtons;
45 - (void)layoutButtons;
46 - (void)updateButtons;
47 - (NSSize)preferredLegalDocumentSizeForWidth:(CGFloat)width;
51 @implementation AutofillMainContainer
53 @synthesize target = target_;
55 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate {
56 if (self = [super init]) {
63 [self buildWindowButtons];
65 base::scoped_nsobject<NSView> view([[NSView alloc] initWithFrame:NSZeroRect]);
66 [view setAutoresizesSubviews:YES];
67 [view setSubviews:@[buttonContainer_]];
70 // Set up Wallet icon.
71 buttonStripImage_.reset([[NSImageView alloc] initWithFrame:NSZeroRect]);
72 [self updateWalletIcon];
73 [[self view] addSubview:buttonStripImage_];
75 // Set up "Save in Chrome" checkbox.
76 saveInChromeCheckbox_.reset([[NSButton alloc] initWithFrame:NSZeroRect]);
77 [saveInChromeCheckbox_ setButtonType:NSSwitchButton];
78 [saveInChromeCheckbox_ setTitle:
79 base::SysUTF16ToNSString(delegate_->SaveLocallyText())];
80 [saveInChromeCheckbox_ sizeToFit];
81 [[self view] addSubview:saveInChromeCheckbox_];
83 saveInChromeTooltip_.reset(
84 [[AutofillTooltipController alloc]
85 initWithArrowLocation:info_bubble::kTopCenter]);
86 [saveInChromeTooltip_ setImage:
87 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
88 IDR_AUTOFILL_TOOLTIP_ICON).ToNSImage()];
89 [saveInChromeTooltip_ setMessage:
90 base::SysUTF16ToNSString(delegate_->SaveLocallyTooltip())];
91 [[self view] addSubview:[saveInChromeTooltip_ view]];
92 [self updateSaveInChrome];
94 detailsContainer_.reset(
95 [[AutofillDetailsContainer alloc] initWithDelegate:delegate_]);
96 NSSize frameSize = [[detailsContainer_ view] frame].size;
97 [[detailsContainer_ view] setFrameOrigin:
98 NSMakePoint(0, NSHeight([buttonContainer_ frame]))];
99 frameSize.height += NSHeight([buttonContainer_ frame]);
100 [[self view] setFrameSize:frameSize];
101 [[self view] addSubview:[detailsContainer_ view]];
103 legalDocumentsView_.reset(
104 [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
105 [legalDocumentsView_ setEditable:NO];
106 [legalDocumentsView_ setBackgroundColor:
107 [NSColor colorWithCalibratedRed:0.96
111 [legalDocumentsView_ setDrawsBackground:YES];
112 [legalDocumentsView_ setTextContainerInset:
113 NSMakeSize(kLegalDocumentsPadding, kLegalDocumentsPadding)];
114 [legalDocumentsView_ setHidden:YES];
115 [legalDocumentsView_ setDelegate:self];
116 legalDocumentsSizeDirty_ = YES;
117 [[self view] addSubview:legalDocumentsView_];
119 notificationContainer_.reset(
120 [[AutofillNotificationContainer alloc] initWithDelegate:delegate_]);
121 [[self view] addSubview:[notificationContainer_ view]];
124 // Called when embedded links are clicked.
125 - (BOOL)textView:(NSTextView*)textView
126 clickedOnLink:(id)link
127 atIndex:(NSUInteger)charIndex {
128 int index = [base::mac::ObjCCastStrict<NSNumber>(link) intValue];
129 delegate_->LegalDocumentLinkClicked(
130 delegate_->LegalDocumentLinks()[index]);
134 - (NSSize)decorationSizeForWidth:(CGFloat)width {
135 NSSize buttonSize = [buttonContainer_ frame].size;
136 NSSize buttonStripImageSize = [buttonStripImage_ frame].size;
137 NSSize buttonStripSize =
138 NSMakeSize(buttonSize.width + chrome_style::kHorizontalPadding +
139 buttonStripImageSize.width,
140 std::max(buttonSize.height + kButtonVerticalPadding,
141 buttonStripImageSize.height) +
142 chrome_style::kClientBottomPadding);
144 NSSize size = NSMakeSize(std::max(buttonStripSize.width, width),
145 buttonStripSize.height);
146 if (![legalDocumentsView_ isHidden]) {
147 NSSize legalDocumentSize =
148 [self preferredLegalDocumentSizeForWidth:width];
149 size.height += legalDocumentSize.height + autofill::kVerticalSpacing;
152 NSSize notificationSize =
153 [notificationContainer_ preferredSizeForWidth:width];
154 size.height += notificationSize.height;
159 - (NSSize)preferredSize {
160 NSSize detailsSize = [detailsContainer_ preferredSize];
161 NSSize decorationSize = [self decorationSizeForWidth:detailsSize.width];
163 NSSize size = NSMakeSize(std::max(decorationSize.width, detailsSize.width),
164 decorationSize.height + detailsSize.height);
165 size.height += autofill::kDetailVerticalPadding;
170 - (void)performLayout {
171 NSRect bounds = [[self view] bounds];
173 CGFloat currentY = 0.0;
174 if (![legalDocumentsView_ isHidden]) {
175 [legalDocumentsView_ setFrameSize:
176 [self preferredLegalDocumentSizeForWidth:NSWidth(bounds)]];
177 currentY = NSMaxY([legalDocumentsView_ frame]) + autofill::kVerticalSpacing;
180 NSRect buttonFrame = [buttonContainer_ frame];
181 buttonFrame.origin.y = currentY + chrome_style::kClientBottomPadding;
182 [buttonContainer_ setFrameOrigin:buttonFrame.origin];
183 currentY = NSMaxY(buttonFrame) + kButtonVerticalPadding;
185 NSPoint walletIconOrigin =
186 NSMakePoint(chrome_style::kHorizontalPadding, buttonFrame.origin.y);
187 [buttonStripImage_ setFrameOrigin:walletIconOrigin];
188 currentY = std::max(currentY, NSMaxY([buttonStripImage_ frame]));
190 NSRect checkboxFrame = [saveInChromeCheckbox_ frame];
191 [saveInChromeCheckbox_ setFrameOrigin:
192 NSMakePoint(chrome_style::kHorizontalPadding,
193 NSMidY(buttonFrame) - NSHeight(checkboxFrame) / 2.0)];
195 NSRect tooltipFrame = [[saveInChromeTooltip_ view] frame];
196 [[saveInChromeTooltip_ view] setFrameOrigin:
197 NSMakePoint(NSMaxX([saveInChromeCheckbox_ frame]) + autofill::kButtonGap,
198 NSMidY(buttonFrame) - (NSHeight(tooltipFrame) / 2.0))];
200 NSRect notificationFrame = NSZeroRect;
201 notificationFrame.size = [notificationContainer_ preferredSizeForWidth:
204 // Buttons/checkbox/legal take up lower part of view, notifications the
205 // upper part. Adjust the detailsContainer to take up the remainder.
206 CGFloat remainingHeight =
207 NSHeight(bounds) - currentY - NSHeight(notificationFrame);
208 NSRect containerFrame =
209 NSMakeRect(0, currentY, NSWidth(bounds), remainingHeight);
210 [[detailsContainer_ view] setFrame:containerFrame];
211 [detailsContainer_ performLayout];
213 notificationFrame.origin = NSMakePoint(0, NSMaxY(containerFrame));
214 [[notificationContainer_ view] setFrame:notificationFrame];
215 [notificationContainer_ performLayout];
218 - (void)buildWindowButtons {
219 if (buttonContainer_.get())
222 buttonContainer_.reset([[GTMWidthBasedTweaker alloc] initWithFrame:
223 ui::kWindowSizeDeterminedLater]);
225 setAutoresizingMask:(NSViewMinXMargin | NSViewMaxYMargin)];
227 base::scoped_nsobject<NSButton> button(
228 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
229 [button setKeyEquivalent:kKeyEquivalentEscape];
230 [button setTarget:target_];
231 [button setAction:@selector(cancel:)];
233 [buttonContainer_ addSubview:button];
235 CGFloat nextX = NSMaxX([button frame]) + autofill::kButtonGap;
236 button.reset([[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
237 [button setFrameOrigin:NSMakePoint(nextX, 0)];
238 [button setKeyEquivalent:kKeyEquivalentReturn];
239 [button setTarget:target_];
240 [button setAction:@selector(accept:)];
241 [buttonContainer_ addSubview:button];
242 [self updateButtons];
244 NSRect frame = NSMakeRect(
245 -NSMaxX([button frame]) - chrome_style::kHorizontalPadding, 0,
246 NSMaxX([button frame]), NSHeight([button frame]));
247 [buttonContainer_ setFrame:frame];
250 - (void)layoutButtons {
251 base::scoped_nsobject<GTMUILocalizerAndLayoutTweaker> layoutTweaker(
252 [[GTMUILocalizerAndLayoutTweaker alloc] init]);
253 [layoutTweaker tweakUI:buttonContainer_];
255 // Now ensure both buttons have the same height. The second button is
256 // known to be the larger one.
257 CGFloat buttonHeight =
258 NSHeight([[[buttonContainer_ subviews] objectAtIndex:1] frame]);
260 // Force first button to be the same height.
261 NSView* button = [[buttonContainer_ subviews] objectAtIndex:0];
262 NSSize buttonSize = [button frame].size;
263 buttonSize.height = buttonHeight;
264 [button setFrameSize:buttonSize];
267 - (void)updateButtons {
268 NSButton* button = base::mac::ObjCCastStrict<NSButton>(
269 [[buttonContainer_ subviews] objectAtIndex:0]);
270 [button setTitle:base::SysUTF16ToNSString(delegate_->CancelButtonText())];
271 button = base::mac::ObjCCastStrict<NSButton>(
272 [[buttonContainer_ subviews] objectAtIndex:1]);
273 [button setTitle:base::SysUTF16ToNSString(delegate_->ConfirmButtonText())];
274 [self layoutButtons];
277 // Compute the preferred size for the legal documents text, given a width.
278 - (NSSize)preferredLegalDocumentSizeForWidth:(CGFloat)width {
279 // Only recompute if necessary (On text or frame width change).
280 if (!legalDocumentsSizeDirty_ &&
281 std::abs(legalDocumentsSize_.width - width) < 1.0) {
282 return legalDocumentsSize_;
285 // There's no direct API to compute desired sizes - use layouting instead.
286 // Layout in a rect with fixed width and "infinite" height.
287 NSRect currentFrame = [legalDocumentsView_ frame];
288 [legalDocumentsView_ setFrame:NSMakeRect(0, 0, width, CGFLOAT_MAX)];
290 // Now use the layout manager to compute layout.
291 NSLayoutManager* layoutManager = [legalDocumentsView_ layoutManager];
292 NSTextContainer* textContainer = [legalDocumentsView_ textContainer];
293 [layoutManager ensureLayoutForTextContainer:textContainer];
294 NSRect newFrame = [layoutManager usedRectForTextContainer:textContainer];
296 // And finally, restore old frame.
297 [legalDocumentsView_ setFrame:currentFrame];
298 newFrame.size.width = width;
300 // Account for the padding around the text.
301 newFrame.size.height += 2 * kLegalDocumentsPadding;
303 legalDocumentsSizeDirty_ = NO;
304 legalDocumentsSize_ = newFrame.size;
305 return legalDocumentsSize_;
308 - (AutofillSectionContainer*)sectionForId:(autofill::DialogSection)section {
309 return [detailsContainer_ sectionForId:section];
312 - (void)modelChanged {
313 [self updateSaveInChrome];
314 [self updateWalletIcon];
315 [self updateButtons];
316 [detailsContainer_ modelChanged];
319 - (BOOL)saveDetailsLocally {
320 return [saveInChromeCheckbox_ state] == NSOnState;
323 - (void)updateLegalDocuments {
324 NSString* text = base::SysUTF16ToNSString(delegate_->LegalDocumentsText());
328 [NSFont labelFontOfSize:[[legalDocumentsView_ font] pointSize]];
329 NSColor* color = gfx::SkColorToCalibratedNSColor(kLegalDocumentsTextColor);
330 [legalDocumentsView_ setMessage:text withFont:font messageColor:color];
332 const std::vector<gfx::Range>& link_ranges =
333 delegate_->LegalDocumentLinks();
334 for (size_t i = 0; i < link_ranges.size(); ++i) {
335 NSRange range = link_ranges[i].ToNSRange();
336 [legalDocumentsView_ addLinkRange:range
338 linkColor:[NSColor blueColor]];
340 legalDocumentsSizeDirty_ = YES;
342 [legalDocumentsView_ setHidden:[text length] == 0];
344 // Always request re-layout on state change.
345 id delegate = [[[self view] window] windowController];
346 if ([delegate respondsToSelector:@selector(requestRelayout)])
347 [delegate performSelector:@selector(requestRelayout)];
350 - (void)updateNotificationArea {
351 [notificationContainer_ setNotifications:delegate_->CurrentNotifications()];
352 id delegate = [[[self view] window] windowController];
353 if ([delegate respondsToSelector:@selector(requestRelayout)])
354 [delegate performSelector:@selector(requestRelayout)];
357 - (void)setAnchorView:(NSView*)anchorView {
358 [notificationContainer_ setAnchorView:anchorView];
362 return [detailsContainer_ validate];
365 - (void)updateSaveInChrome {
366 [saveInChromeCheckbox_ setHidden:!delegate_->ShouldOfferToSaveInChrome()];
367 [[saveInChromeTooltip_ view] setHidden:[saveInChromeCheckbox_ isHidden]];
368 [saveInChromeCheckbox_ setState:
369 (delegate_->ShouldSaveInChrome() ? NSOnState : NSOffState)];
372 - (void)makeFirstInvalidInputFirstResponder {
373 NSView* field = [detailsContainer_ firstInvalidField];
377 [detailsContainer_ scrollToView:field];
378 [[[self view] window] makeFirstResponder:field];
381 - (void)updateWalletIcon {
382 gfx::Image image = delegate_->ButtonStripImage();
383 [buttonStripImage_ setHidden:image.IsEmpty()];
384 if (![buttonStripImage_ isHidden]) {
385 [buttonStripImage_ setImage:image.ToNSImage()];
386 [buttonStripImage_ setFrameSize:[[buttonStripImage_ image] size]];
390 - (void)scrollInitialEditorIntoViewAndMakeFirstResponder {
391 // Try to focus on the first invalid field. If there isn't one, focus on the
392 // first editable field instead.
393 NSView* field = [detailsContainer_ firstInvalidField];
395 field = [detailsContainer_ firstVisibleField];
399 [detailsContainer_ scrollToView:field];
400 [[[self view] window] makeFirstResponder:field];
403 - (void)updateErrorBubble {
404 [detailsContainer_ updateErrorBubble];
410 @implementation AutofillMainContainer (Testing)
412 - (NSButton*)saveInChromeCheckboxForTesting {
413 return saveInChromeCheckbox_.get();
416 - (NSImageView*)buttonStripImageForTesting {
417 return buttonStripImage_.get();
420 - (NSButton*)saveInChromeTooltipForTesting {
421 return base::mac::ObjCCast<NSButton>([saveInChromeTooltip_ view]);