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 #include "chrome/browser/ui/cocoa/autofill/autofill_dialog_constants.h"
15 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_button.h"
16 #import "chrome/browser/ui/cocoa/autofill/autofill_details_container.h"
17 #import "chrome/browser/ui/cocoa/autofill/autofill_notification_container.h"
18 #import "chrome/browser/ui/cocoa/autofill/autofill_tooltip_controller.h"
19 #import "chrome/browser/ui/cocoa/hyperlink_text_view.h"
20 #import "chrome/browser/ui/cocoa/key_equivalent_constants.h"
21 #include "grit/generated_resources.h"
22 #include "grit/theme_resources.h"
23 #include "skia/ext/skia_utils_mac.h"
24 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
25 #import "ui/base/cocoa/controls/blue_label_button.h"
26 #include "ui/base/cocoa/window_size_constants.h"
27 #include "ui/base/l10n/l10n_util.h"
28 #include "ui/gfx/range/range.h"
32 // Padding between buttons and the last suggestion or details view. The mock
33 // has a total of 30px - but 10px are already provided by details/suggestions.
34 const CGFloat kButtonVerticalPadding = 20.0;
36 // Padding around the text for the legal documents.
37 const CGFloat kLegalDocumentsPadding = 20.0;
39 // The font color for the legal documents text. Set to match the Views
41 const SkColor kLegalDocumentsTextColor = SkColorSetRGB(102, 102, 102);
45 @interface AutofillMainContainer (Private)
46 - (void)buildWindowButtons;
47 - (void)layoutButtons;
48 - (void)updateButtons;
49 - (NSSize)preferredLegalDocumentSizeForWidth:(CGFloat)width;
53 @implementation AutofillMainContainer
55 @synthesize target = target_;
57 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate {
58 if (self = [super init]) {
65 [self buildWindowButtons];
67 base::scoped_nsobject<NSView> view([[NSView alloc] initWithFrame:NSZeroRect]);
68 [view setAutoresizesSubviews:YES];
69 [view setSubviews:@[buttonContainer_]];
72 // Set up Wallet icon.
73 buttonStripImage_.reset([[NSImageView alloc] initWithFrame:NSZeroRect]);
74 [self updateWalletIcon];
75 [[self view] addSubview:buttonStripImage_];
77 // Set up "Save in Chrome" checkbox.
78 saveInChromeCheckbox_.reset([[NSButton alloc] initWithFrame:NSZeroRect]);
79 [saveInChromeCheckbox_ setButtonType:NSSwitchButton];
80 [saveInChromeCheckbox_ setTitle:
81 base::SysUTF16ToNSString(delegate_->SaveLocallyText())];
82 [saveInChromeCheckbox_ sizeToFit];
83 [[self view] addSubview:saveInChromeCheckbox_];
85 saveInChromeTooltip_.reset(
86 [[AutofillTooltipController alloc]
87 initWithArrowLocation:info_bubble::kTopCenter]);
88 [saveInChromeTooltip_ setImage:
89 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
90 IDR_AUTOFILL_TOOLTIP_ICON).ToNSImage()];
91 [saveInChromeTooltip_ setMessage:
92 base::SysUTF16ToNSString(delegate_->SaveLocallyTooltip())];
93 [[self view] addSubview:[saveInChromeTooltip_ view]];
94 [self updateSaveInChrome];
96 detailsContainer_.reset(
97 [[AutofillDetailsContainer alloc] initWithDelegate:delegate_]);
98 NSSize frameSize = [[detailsContainer_ view] frame].size;
99 [[detailsContainer_ view] setFrameOrigin:
100 NSMakePoint(0, NSHeight([buttonContainer_ frame]))];
101 frameSize.height += NSHeight([buttonContainer_ frame]);
102 [[self view] setFrameSize:frameSize];
103 [[self view] addSubview:[detailsContainer_ view]];
105 legalDocumentsView_.reset(
106 [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
107 [legalDocumentsView_ setEditable:NO];
108 [legalDocumentsView_ setBackgroundColor:
109 [NSColor colorWithCalibratedRed:0.96
113 [legalDocumentsView_ setDrawsBackground:YES];
114 [legalDocumentsView_ setTextContainerInset:
115 NSMakeSize(kLegalDocumentsPadding, kLegalDocumentsPadding)];
116 [legalDocumentsView_ setHidden:YES];
117 [legalDocumentsView_ setDelegate:self];
118 legalDocumentsSizeDirty_ = YES;
119 [[self view] addSubview:legalDocumentsView_];
121 notificationContainer_.reset(
122 [[AutofillNotificationContainer alloc] initWithDelegate:delegate_]);
123 [[self view] addSubview:[notificationContainer_ view]];
126 // Called when embedded links are clicked.
127 - (BOOL)textView:(NSTextView*)textView
128 clickedOnLink:(id)link
129 atIndex:(NSUInteger)charIndex {
130 int index = [base::mac::ObjCCastStrict<NSNumber>(link) intValue];
131 delegate_->LegalDocumentLinkClicked(
132 delegate_->LegalDocumentLinks()[index]);
136 - (NSSize)decorationSizeForWidth:(CGFloat)width {
137 NSSize buttonSize = [buttonContainer_ frame].size;
138 NSSize buttonStripImageSize = [buttonStripImage_ frame].size;
139 NSSize buttonStripSize =
140 NSMakeSize(buttonSize.width + chrome_style::kHorizontalPadding +
141 buttonStripImageSize.width,
142 std::max(buttonSize.height + kButtonVerticalPadding,
143 buttonStripImageSize.height) +
144 chrome_style::kClientBottomPadding);
146 NSSize size = NSMakeSize(std::max(buttonStripSize.width, width),
147 buttonStripSize.height);
148 if (![legalDocumentsView_ isHidden]) {
149 NSSize legalDocumentSize =
150 [self preferredLegalDocumentSizeForWidth:width];
151 size.height += legalDocumentSize.height + autofill::kVerticalSpacing;
154 NSSize notificationSize =
155 [notificationContainer_ preferredSizeForWidth:width];
156 size.height += notificationSize.height;
161 - (NSSize)preferredSize {
162 NSSize detailsSize = [detailsContainer_ preferredSize];
163 NSSize decorationSize = [self decorationSizeForWidth:detailsSize.width];
165 NSSize size = NSMakeSize(std::max(decorationSize.width, detailsSize.width),
166 decorationSize.height + detailsSize.height);
167 size.height += autofill::kDetailVerticalPadding;
172 - (void)performLayout {
173 NSRect bounds = [[self view] bounds];
175 CGFloat currentY = 0.0;
176 if (![legalDocumentsView_ isHidden]) {
177 [legalDocumentsView_ setFrameSize:
178 [self preferredLegalDocumentSizeForWidth:NSWidth(bounds)]];
179 currentY = NSMaxY([legalDocumentsView_ frame]) + autofill::kVerticalSpacing;
182 NSRect buttonFrame = [buttonContainer_ frame];
183 buttonFrame.origin.y = currentY + chrome_style::kClientBottomPadding;
184 [buttonContainer_ setFrameOrigin:buttonFrame.origin];
185 currentY = NSMaxY(buttonFrame) + kButtonVerticalPadding;
187 NSPoint walletIconOrigin =
188 NSMakePoint(chrome_style::kHorizontalPadding, buttonFrame.origin.y);
189 [buttonStripImage_ setFrameOrigin:walletIconOrigin];
190 currentY = std::max(currentY, NSMaxY([buttonStripImage_ frame]));
192 NSRect checkboxFrame = [saveInChromeCheckbox_ frame];
193 [saveInChromeCheckbox_ setFrameOrigin:
194 NSMakePoint(chrome_style::kHorizontalPadding,
195 NSMidY(buttonFrame) - NSHeight(checkboxFrame) / 2.0)];
197 NSRect tooltipFrame = [[saveInChromeTooltip_ view] frame];
198 [[saveInChromeTooltip_ view] setFrameOrigin:
199 NSMakePoint(NSMaxX([saveInChromeCheckbox_ frame]) + autofill::kButtonGap,
200 NSMidY(buttonFrame) - (NSHeight(tooltipFrame) / 2.0))];
202 NSRect notificationFrame = NSZeroRect;
203 notificationFrame.size = [notificationContainer_ preferredSizeForWidth:
206 // Buttons/checkbox/legal take up lower part of view, notifications the
207 // upper part. Adjust the detailsContainer to take up the remainder.
208 CGFloat remainingHeight =
209 NSHeight(bounds) - currentY - NSHeight(notificationFrame);
210 NSRect containerFrame =
211 NSMakeRect(0, currentY, NSWidth(bounds), remainingHeight);
212 [[detailsContainer_ view] setFrame:containerFrame];
213 [detailsContainer_ performLayout];
215 notificationFrame.origin = NSMakePoint(0, NSMaxY(containerFrame));
216 [[notificationContainer_ view] setFrame:notificationFrame];
217 [notificationContainer_ performLayout];
220 - (void)buildWindowButtons {
221 if (buttonContainer_.get())
224 buttonContainer_.reset([[GTMWidthBasedTweaker alloc] initWithFrame:
225 ui::kWindowSizeDeterminedLater]);
227 setAutoresizingMask:(NSViewMinXMargin | NSViewMaxYMargin)];
229 base::scoped_nsobject<NSButton> button(
230 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
231 [button setKeyEquivalent:kKeyEquivalentEscape];
232 [button setTarget:target_];
233 [button setAction:@selector(cancel:)];
235 [buttonContainer_ addSubview:button];
237 CGFloat nextX = NSMaxX([button frame]) + autofill::kButtonGap;
238 button.reset([[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
239 [button setFrameOrigin:NSMakePoint(nextX, 0)];
240 [button setKeyEquivalent:kKeyEquivalentReturn];
241 [button setTarget:target_];
242 [button setAction:@selector(accept:)];
243 [buttonContainer_ addSubview:button];
244 [self updateButtons];
246 NSRect frame = NSMakeRect(
247 -NSMaxX([button frame]) - chrome_style::kHorizontalPadding, 0,
248 NSMaxX([button frame]), NSHeight([button frame]));
249 [buttonContainer_ setFrame:frame];
252 - (void)layoutButtons {
253 base::scoped_nsobject<GTMUILocalizerAndLayoutTweaker> layoutTweaker(
254 [[GTMUILocalizerAndLayoutTweaker alloc] init]);
255 [layoutTweaker tweakUI:buttonContainer_];
257 // Now ensure both buttons have the same height. The second button is
258 // known to be the larger one.
259 CGFloat buttonHeight =
260 NSHeight([[[buttonContainer_ subviews] objectAtIndex:1] frame]);
262 // Force first button to be the same height.
263 NSView* button = [[buttonContainer_ subviews] objectAtIndex:0];
264 NSSize buttonSize = [button frame].size;
265 buttonSize.height = buttonHeight;
266 [button setFrameSize:buttonSize];
269 - (void)updateButtons {
270 NSButton* button = base::mac::ObjCCastStrict<NSButton>(
271 [[buttonContainer_ subviews] objectAtIndex:0]);
272 [button setTitle:base::SysUTF16ToNSString(delegate_->CancelButtonText())];
273 button = base::mac::ObjCCastStrict<NSButton>(
274 [[buttonContainer_ subviews] objectAtIndex:1]);
275 [button setTitle:base::SysUTF16ToNSString(delegate_->ConfirmButtonText())];
276 [self layoutButtons];
279 // Compute the preferred size for the legal documents text, given a width.
280 - (NSSize)preferredLegalDocumentSizeForWidth:(CGFloat)width {
281 // Only recompute if necessary (On text or frame width change).
282 if (!legalDocumentsSizeDirty_ && abs(legalDocumentsSize_.width-width) < 1.0)
283 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]);