blob: a74650bca04ed87c536a6436070be698995cfb48 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/autofill/autofill_main_container.h"
#include <algorithm>
#include <cmath>
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h"
#include "chrome/browser/ui/chrome_style.h"
#include "chrome/browser/ui/cocoa/autofill/autofill_dialog_constants.h"
#import "chrome/browser/ui/cocoa/constrained_window/constrained_window_button.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_details_container.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_notification_container.h"
#import "chrome/browser/ui/cocoa/autofill/autofill_tooltip_controller.h"
#import "chrome/browser/ui/cocoa/hyperlink_text_view.h"
#import "chrome/browser/ui/cocoa/key_equivalent_constants.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "skia/ext/skia_utils_mac.h"
#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
#import "ui/base/cocoa/controls/blue_label_button.h"
#include "ui/base/cocoa/window_size_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/range/range.h"
namespace {
// Padding between buttons and the last suggestion or details view. The mock
// has a total of 30px - but 10px are already provided by details/suggestions.
const CGFloat kButtonVerticalPadding = 20.0;
// Padding around the text for the legal documents.
const CGFloat kLegalDocumentsPadding = 20.0;
// The font color for the legal documents text. Set to match the Views
// implementation.
const SkColor kLegalDocumentsTextColor = SkColorSetRGB(102, 102, 102);
} // namespace
@interface AutofillMainContainer (Private)
- (void)buildWindowButtons;
- (void)layoutButtons;
- (void)updateButtons;
- (NSSize)preferredLegalDocumentSizeForWidth:(CGFloat)width;
@end
@implementation AutofillMainContainer
@synthesize target = target_;
- (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate {
if (self = [super init]) {
delegate_ = delegate;
}
return self;
}
- (void)loadView {
[self buildWindowButtons];
base::scoped_nsobject<NSView> view([[NSView alloc] initWithFrame:NSZeroRect]);
[view setAutoresizesSubviews:YES];
[view setSubviews:@[buttonContainer_]];
[self setView:view];
// Set up Wallet icon.
buttonStripImage_.reset([[NSImageView alloc] initWithFrame:NSZeroRect]);
[self updateWalletIcon];
[[self view] addSubview:buttonStripImage_];
// Set up "Save in Chrome" checkbox.
saveInChromeCheckbox_.reset([[NSButton alloc] initWithFrame:NSZeroRect]);
[saveInChromeCheckbox_ setButtonType:NSSwitchButton];
[saveInChromeCheckbox_ setTitle:
base::SysUTF16ToNSString(delegate_->SaveLocallyText())];
[saveInChromeCheckbox_ sizeToFit];
[[self view] addSubview:saveInChromeCheckbox_];
saveInChromeTooltip_.reset(
[[AutofillTooltipController alloc] init]);
[saveInChromeTooltip_ setImage:
ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
IDR_AUTOFILL_TOOLTIP_ICON).ToNSImage()];
[saveInChromeTooltip_ setMessage:
base::SysUTF16ToNSString(delegate_->SaveLocallyTooltip())];
[[self view] addSubview:[saveInChromeTooltip_ view]];
[self updateSaveInChrome];
detailsContainer_.reset(
[[AutofillDetailsContainer alloc] initWithDelegate:delegate_]);
NSSize frameSize = [[detailsContainer_ view] frame].size;
[[detailsContainer_ view] setFrameOrigin:
NSMakePoint(0, NSHeight([buttonContainer_ frame]))];
frameSize.height += NSHeight([buttonContainer_ frame]);
[[self view] setFrameSize:frameSize];
[[self view] addSubview:[detailsContainer_ view]];
legalDocumentsView_.reset(
[[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
[legalDocumentsView_ setEditable:NO];
[legalDocumentsView_ setBackgroundColor:
[NSColor colorWithCalibratedRed:0.96
green:0.96
blue:0.96
alpha:1.0]];
[legalDocumentsView_ setDrawsBackground:YES];
[legalDocumentsView_ setTextContainerInset:
NSMakeSize(kLegalDocumentsPadding, kLegalDocumentsPadding)];
[legalDocumentsView_ setHidden:YES];
[legalDocumentsView_ setDelegate:self];
legalDocumentsSizeDirty_ = YES;
[[self view] addSubview:legalDocumentsView_];
notificationContainer_.reset(
[[AutofillNotificationContainer alloc] initWithDelegate:delegate_]);
[[self view] addSubview:[notificationContainer_ view]];
}
// Called when embedded links are clicked.
- (BOOL)textView:(NSTextView*)textView
clickedOnLink:(id)link
atIndex:(NSUInteger)charIndex {
int index = [base::mac::ObjCCastStrict<NSNumber>(link) intValue];
delegate_->LegalDocumentLinkClicked(
delegate_->LegalDocumentLinks()[index]);
return YES;
}
- (NSSize)decorationSizeForWidth:(CGFloat)width {
NSSize buttonSize = [buttonContainer_ frame].size;
NSSize buttonStripImageSize = [buttonStripImage_ frame].size;
NSSize buttonStripSize =
NSMakeSize(buttonSize.width + chrome_style::kHorizontalPadding +
buttonStripImageSize.width,
std::max(buttonSize.height + kButtonVerticalPadding,
buttonStripImageSize.height) +
chrome_style::kClientBottomPadding);
NSSize size = NSMakeSize(std::max(buttonStripSize.width, width),
buttonStripSize.height);
if (![legalDocumentsView_ isHidden]) {
NSSize legalDocumentSize =
[self preferredLegalDocumentSizeForWidth:width];
size.height += legalDocumentSize.height + autofill::kVerticalSpacing;
}
NSSize notificationSize =
[notificationContainer_ preferredSizeForWidth:width];
size.height += notificationSize.height;
return size;
}
- (NSSize)preferredSize {
NSSize detailsSize = [detailsContainer_ preferredSize];
NSSize decorationSize = [self decorationSizeForWidth:detailsSize.width];
NSSize size = NSMakeSize(std::max(decorationSize.width, detailsSize.width),
decorationSize.height + detailsSize.height);
size.height += autofill::kDetailVerticalPadding;
return size;
}
- (void)performLayout {
NSRect bounds = [[self view] bounds];
CGFloat currentY = 0.0;
if (![legalDocumentsView_ isHidden]) {
[legalDocumentsView_ setFrameSize:
[self preferredLegalDocumentSizeForWidth:NSWidth(bounds)]];
currentY = NSMaxY([legalDocumentsView_ frame]) + autofill::kVerticalSpacing;
}
NSRect buttonFrame = [buttonContainer_ frame];
buttonFrame.origin.y = currentY + chrome_style::kClientBottomPadding;
[buttonContainer_ setFrameOrigin:buttonFrame.origin];
currentY = NSMaxY(buttonFrame) + kButtonVerticalPadding;
NSPoint walletIconOrigin =
NSMakePoint(chrome_style::kHorizontalPadding, buttonFrame.origin.y);
[buttonStripImage_ setFrameOrigin:walletIconOrigin];
currentY = std::max(currentY, NSMaxY([buttonStripImage_ frame]));
NSRect checkboxFrame = [saveInChromeCheckbox_ frame];
[saveInChromeCheckbox_ setFrameOrigin:
NSMakePoint(chrome_style::kHorizontalPadding,
NSMidY(buttonFrame) - NSHeight(checkboxFrame) / 2.0)];
NSRect tooltipFrame = [[saveInChromeTooltip_ view] frame];
[[saveInChromeTooltip_ view] setFrameOrigin:
NSMakePoint(NSMaxX([saveInChromeCheckbox_ frame]) + autofill::kButtonGap,
NSMidY(buttonFrame) - (NSHeight(tooltipFrame) / 2.0))];
NSRect notificationFrame = NSZeroRect;
notificationFrame.size = [notificationContainer_ preferredSizeForWidth:
NSWidth(bounds)];
// Buttons/checkbox/legal take up lower part of view, notifications the
// upper part. Adjust the detailsContainer to take up the remainder.
CGFloat remainingHeight =
NSHeight(bounds) - currentY - NSHeight(notificationFrame);
NSRect containerFrame =
NSMakeRect(0, currentY, NSWidth(bounds), remainingHeight);
[[detailsContainer_ view] setFrame:containerFrame];
[detailsContainer_ performLayout];
notificationFrame.origin = NSMakePoint(0, NSMaxY(containerFrame));
[[notificationContainer_ view] setFrame:notificationFrame];
[notificationContainer_ performLayout];
}
- (void)buildWindowButtons {
if (buttonContainer_.get())
return;
buttonContainer_.reset([[GTMWidthBasedTweaker alloc] initWithFrame:
ui::kWindowSizeDeterminedLater]);
[buttonContainer_
setAutoresizingMask:(NSViewMinXMargin | NSViewMaxYMargin)];
base::scoped_nsobject<NSButton> button(
[[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
[button setKeyEquivalent:kKeyEquivalentEscape];
[button setTarget:target_];
[button setAction:@selector(cancel:)];
[button sizeToFit];
[buttonContainer_ addSubview:button];
CGFloat nextX = NSMaxX([button frame]) + autofill::kButtonGap;
button.reset([[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
[button setFrameOrigin:NSMakePoint(nextX, 0)];
[button setKeyEquivalent:kKeyEquivalentReturn];
[button setTarget:target_];
[button setAction:@selector(accept:)];
[buttonContainer_ addSubview:button];
[self updateButtons];
NSRect frame = NSMakeRect(
-NSMaxX([button frame]) - chrome_style::kHorizontalPadding, 0,
NSMaxX([button frame]), NSHeight([button frame]));
[buttonContainer_ setFrame:frame];
}
- (void)layoutButtons {
base::scoped_nsobject<GTMUILocalizerAndLayoutTweaker> layoutTweaker(
[[GTMUILocalizerAndLayoutTweaker alloc] init]);
[layoutTweaker tweakUI:buttonContainer_];
// Now ensure both buttons have the same height. The second button is
// known to be the larger one.
CGFloat buttonHeight =
NSHeight([[[buttonContainer_ subviews] objectAtIndex:1] frame]);
// Account for a rendering issue in BlueLabelButton.
// TODO(groby): Fix that rendering instead.
buttonHeight -= 1.0;
// Force first button to be the same height.
NSView* button = [[buttonContainer_ subviews] objectAtIndex:0];
NSSize buttonSize = [button frame].size;
buttonSize.height = buttonHeight;
[button setFrameSize:buttonSize];
}
- (void)updateButtons {
NSButton* button = base::mac::ObjCCastStrict<NSButton>(
[[buttonContainer_ subviews] objectAtIndex:0]);
[button setTitle:base::SysUTF16ToNSString(delegate_->CancelButtonText())];
button = base::mac::ObjCCastStrict<NSButton>(
[[buttonContainer_ subviews] objectAtIndex:1]);
[button setTitle:base::SysUTF16ToNSString(delegate_->ConfirmButtonText())];
[self layoutButtons];
}
// Compute the preferred size for the legal documents text, given a width.
- (NSSize)preferredLegalDocumentSizeForWidth:(CGFloat)width {
// Only recompute if necessary (On text or frame width change).
if (!legalDocumentsSizeDirty_ && abs(legalDocumentsSize_.width-width) < 1.0)
return legalDocumentsSize_;
// There's no direct API to compute desired sizes - use layouting instead.
// Layout in a rect with fixed width and "infinite" height.
NSRect currentFrame = [legalDocumentsView_ frame];
[legalDocumentsView_ setFrame:NSMakeRect(0, 0, width, CGFLOAT_MAX)];
// Now use the layout manager to compute layout.
NSLayoutManager* layoutManager = [legalDocumentsView_ layoutManager];
NSTextContainer* textContainer = [legalDocumentsView_ textContainer];
[layoutManager ensureLayoutForTextContainer:textContainer];
NSRect newFrame = [layoutManager usedRectForTextContainer:textContainer];
// And finally, restore old frame.
[legalDocumentsView_ setFrame:currentFrame];
newFrame.size.width = width;
// Account for the padding around the text.
newFrame.size.height += 2 * kLegalDocumentsPadding;
legalDocumentsSizeDirty_ = NO;
legalDocumentsSize_ = newFrame.size;
return legalDocumentsSize_;
}
- (AutofillSectionContainer*)sectionForId:(autofill::DialogSection)section {
return [detailsContainer_ sectionForId:section];
}
- (void)modelChanged {
[self updateSaveInChrome];
[self updateWalletIcon];
[self updateButtons];
[detailsContainer_ modelChanged];
}
- (BOOL)saveDetailsLocally {
return [saveInChromeCheckbox_ state] == NSOnState;
}
- (void)updateLegalDocuments {
NSString* text = base::SysUTF16ToNSString(delegate_->LegalDocumentsText());
if ([text length]) {
NSFont* font =
[NSFont labelFontOfSize:[[legalDocumentsView_ font] pointSize]];
NSColor* color = gfx::SkColorToCalibratedNSColor(kLegalDocumentsTextColor);
[legalDocumentsView_ setMessage:text withFont:font messageColor:color];
const std::vector<gfx::Range>& link_ranges =
delegate_->LegalDocumentLinks();
for (size_t i = 0; i < link_ranges.size(); ++i) {
NSRange range = link_ranges[i].ToNSRange();
[legalDocumentsView_ addLinkRange:range
withName:@(i)
linkColor:[NSColor blueColor]];
}
legalDocumentsSizeDirty_ = YES;
}
[legalDocumentsView_ setHidden:[text length] == 0];
// Always request re-layout on state change.
id delegate = [[[self view] window] windowController];
if ([delegate respondsToSelector:@selector(requestRelayout)])
[delegate performSelector:@selector(requestRelayout)];
}
- (void)updateNotificationArea {
[notificationContainer_ setNotifications:delegate_->CurrentNotifications()];
id delegate = [[[self view] window] windowController];
if ([delegate respondsToSelector:@selector(requestRelayout)])
[delegate performSelector:@selector(requestRelayout)];
}
- (void)setAnchorView:(NSView*)anchorView {
[notificationContainer_ setAnchorView:anchorView];
}
- (BOOL)validate {
return [detailsContainer_ validate];
}
- (void)updateSaveInChrome {
[saveInChromeCheckbox_ setHidden:!delegate_->ShouldOfferToSaveInChrome()];
[[saveInChromeTooltip_ view] setHidden:[saveInChromeCheckbox_ isHidden]];
[saveInChromeCheckbox_ setState:
(delegate_->ShouldSaveInChrome() ? NSOnState : NSOffState)];
}
- (void)makeFirstInvalidInputFirstResponder {
NSView* field = [detailsContainer_ firstInvalidField];
if (!field)
return;
[detailsContainer_ scrollToView:field];
[[[self view] window] makeFirstResponder:field];
}
- (void)updateWalletIcon {
gfx::Image image = delegate_->ButtonStripImage();
[buttonStripImage_ setHidden:image.IsEmpty()];
if (![buttonStripImage_ isHidden]) {
[buttonStripImage_ setImage:image.ToNSImage()];
[buttonStripImage_ setFrameSize:[[buttonStripImage_ image] size]];
}
}
- (void)updateErrorBubble {
[detailsContainer_ updateErrorBubble];
}
@end
@implementation AutofillMainContainer (Testing)
- (NSButton*)saveInChromeCheckboxForTesting {
return saveInChromeCheckbox_.get();
}
- (NSImageView*)buttonStripImageForTesting {
return buttonStripImage_.get();
}
- (NSButton*)saveInChromeTooltipForTesting {
return base::mac::ObjCCast<NSButton>([saveInChromeTooltip_ view]);
}
@end