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_ &&
283 std::abs(legalDocumentsSize_.width - width) < 1.0) {
284 return legalDocumentsSize_;
287 // There's no direct API to compute desired sizes - use layouting instead.
288 // Layout in a rect with fixed width and "infinite" height.
289 NSRect currentFrame = [legalDocumentsView_ frame];
290 [legalDocumentsView_ setFrame:NSMakeRect(0, 0, width, CGFLOAT_MAX)];
292 // Now use the layout manager to compute layout.
293 NSLayoutManager* layoutManager = [legalDocumentsView_ layoutManager];
294 NSTextContainer* textContainer = [legalDocumentsView_ textContainer];
295 [layoutManager ensureLayoutForTextContainer:textContainer];
296 NSRect newFrame = [layoutManager usedRectForTextContainer:textContainer];
298 // And finally, restore old frame.
299 [legalDocumentsView_ setFrame:currentFrame];
300 newFrame.size.width = width;
302 // Account for the padding around the text.
303 newFrame.size.height += 2 * kLegalDocumentsPadding;
305 legalDocumentsSizeDirty_ = NO;
306 legalDocumentsSize_ = newFrame.size;
307 return legalDocumentsSize_;
310 - (AutofillSectionContainer*)sectionForId:(autofill::DialogSection)section {
311 return [detailsContainer_ sectionForId:section];
314 - (void)modelChanged {
315 [self updateSaveInChrome];
316 [self updateWalletIcon];
317 [self updateButtons];
318 [detailsContainer_ modelChanged];
321 - (BOOL)saveDetailsLocally {
322 return [saveInChromeCheckbox_ state] == NSOnState;
325 - (void)updateLegalDocuments {
326 NSString* text = base::SysUTF16ToNSString(delegate_->LegalDocumentsText());
330 [NSFont labelFontOfSize:[[legalDocumentsView_ font] pointSize]];
331 NSColor* color = gfx::SkColorToCalibratedNSColor(kLegalDocumentsTextColor);
332 [legalDocumentsView_ setMessage:text withFont:font messageColor:color];
334 const std::vector<gfx::Range>& link_ranges =
335 delegate_->LegalDocumentLinks();
336 for (size_t i = 0; i < link_ranges.size(); ++i) {
337 NSRange range = link_ranges[i].ToNSRange();
338 [legalDocumentsView_ addLinkRange:range
340 linkColor:[NSColor blueColor]];
342 legalDocumentsSizeDirty_ = YES;
344 [legalDocumentsView_ setHidden:[text length] == 0];
346 // Always request re-layout on state change.
347 id delegate = [[[self view] window] windowController];
348 if ([delegate respondsToSelector:@selector(requestRelayout)])
349 [delegate performSelector:@selector(requestRelayout)];
352 - (void)updateNotificationArea {
353 [notificationContainer_ setNotifications:delegate_->CurrentNotifications()];
354 id delegate = [[[self view] window] windowController];
355 if ([delegate respondsToSelector:@selector(requestRelayout)])
356 [delegate performSelector:@selector(requestRelayout)];
359 - (void)setAnchorView:(NSView*)anchorView {
360 [notificationContainer_ setAnchorView:anchorView];
364 return [detailsContainer_ validate];
367 - (void)updateSaveInChrome {
368 [saveInChromeCheckbox_ setHidden:!delegate_->ShouldOfferToSaveInChrome()];
369 [[saveInChromeTooltip_ view] setHidden:[saveInChromeCheckbox_ isHidden]];
370 [saveInChromeCheckbox_ setState:
371 (delegate_->ShouldSaveInChrome() ? NSOnState : NSOffState)];
374 - (void)makeFirstInvalidInputFirstResponder {
375 NSView* field = [detailsContainer_ firstInvalidField];
379 [detailsContainer_ scrollToView:field];
380 [[[self view] window] makeFirstResponder:field];
383 - (void)updateWalletIcon {
384 gfx::Image image = delegate_->ButtonStripImage();
385 [buttonStripImage_ setHidden:image.IsEmpty()];
386 if (![buttonStripImage_ isHidden]) {
387 [buttonStripImage_ setImage:image.ToNSImage()];
388 [buttonStripImage_ setFrameSize:[[buttonStripImage_ image] size]];
392 - (void)scrollInitialEditorIntoViewAndMakeFirstResponder {
393 // Try to focus on the first invalid field. If there isn't one, focus on the
394 // first editable field instead.
395 NSView* field = [detailsContainer_ firstInvalidField];
397 field = [detailsContainer_ firstVisibleField];
401 [detailsContainer_ scrollToView:field];
402 [[[self view] window] makeFirstResponder:field];
405 - (void)updateErrorBubble {
406 [detailsContainer_ updateErrorBubble];
412 @implementation AutofillMainContainer (Testing)
414 - (NSButton*)saveInChromeCheckboxForTesting {
415 return saveInChromeCheckbox_.get();
418 - (NSImageView*)buttonStripImageForTesting {
419 return buttonStripImage_.get();
422 - (NSButton*)saveInChromeTooltipForTesting {
423 return base::mac::ObjCCast<NSButton>([saveInChromeTooltip_ view]);