blob: fa4d39a1e88dc7ba0b54ed9bd7b17f084465d987 [file] [log] [blame]
// Copyright 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_suggestion_container.h"
#include <algorithm>
#include <cmath>
#include "base/logging.h"
#include "base/mac/scoped_nsobject.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/autofill/autofill_textfield.h"
#include "skia/ext/skia_utils_mac.h"
namespace {
// Horizontal padding between text and other elements (in pixels).
const int kAroundTextPadding = 4;
// Padding at the top of suggestions.
const CGFloat kTopPadding = 10;
// Indicates infinite size in either vertical or horizontal direction.
// Technically, CGFLOAT_MAX should do. Practically, it runs into several issues.
// #1) Many computations on Retina devices overflow with that value.
// #2) In this particular use case, it results in the message
// "CGAffineTransformInvert: singular matrix."
const CGFloat kInfiniteSize = 1.0e6;
// A line fragment padding that creates the same visual look as text layout in
// an NSTextField does. (Which UX feedback was based on)
const CGFloat kLineFragmentPadding = 2.0;
// Padding added on top of the label so its first line looks centered with
// respect to the input field. Only added when the input field is showing.
const CGFloat kLabelWithInputTopPadding = 5.0;
}
// An attachment cell for a single icon - takes care of proper alignment of
// text and icon.
@interface IconAttachmentCell : NSTextAttachmentCell {
CGFloat baseline_; // The cell's baseline adjustment.
}
// Adjust the cell's baseline so that the lower edge of the image aligns with
// the longest descender, not the font baseline
- (void)adjustBaselineForFont:(NSFont*)font;
@end
@interface AutofillSuggestionView : NSView {
@private
// The main input field - only view not ignoring mouse events.
NSView* inputField_;
}
@property (assign, nonatomic) NSView* inputField;
@end
// The suggestion container should ignore any mouse events unless they occur
// within the bounds of an editable field.
@implementation AutofillSuggestionView
@synthesize inputField = inputField_;
- (NSView*)hitTest:(NSPoint)point {
NSView* hitView = [super hitTest:point];
if ([hitView isDescendantOf:inputField_])
return hitView;
return nil;
}
@end
@implementation IconAttachmentCell
- (NSPoint)cellBaselineOffset {
return NSMakePoint(0.0, baseline_);
}
// Ensure proper padding between text and icon.
- (NSSize)cellSize {
NSSize size = [super cellSize];
size.width += kAroundTextPadding;
return size;
}
// drawWithFrame: needs to be overridden to left-align the image. Default
// rendering centers images in the cell's frame.
- (void)drawWithFrame:(NSRect)frame inView:(NSView*)view {
frame.size.width -= kAroundTextPadding;
[super drawWithFrame:frame inView:view];
}
- (void)adjustBaselineForFont:(NSFont*)font {
CGFloat lineHeight = [font ascender];
baseline_ = std::floor((lineHeight - [[self image] size].height) / 2.0);
}
@end
@interface AutofillSuggestionContainer (Private)
// Set the main suggestion text and the corresponding |icon|.
// Attempts to wrap the text if |wrapText| is set.
- (void)setSuggestionText:(NSString*)line
icon:(NSImage*)icon
wrapText:(BOOL)wrapText;
@end
@implementation AutofillSuggestionContainer
- (AutofillTextField*)inputField {
return inputField_.get();
}
- (NSTextField*)makeDetailSectionLabel:(NSString*)labelText {
base::scoped_nsobject<NSTextField> label([[NSTextField alloc] init]);
[label setFont:
[[NSFontManager sharedFontManager] convertFont:[label font]
toHaveTrait:NSBoldFontMask]];
[label setStringValue:labelText];
[label setEditable:NO];
[label setBordered:NO];
[label sizeToFit];
return label.autorelease();
}
- (void)loadView {
label_.reset([[NSTextView alloc] initWithFrame:NSZeroRect]);
[[label_ textContainer] setLineFragmentPadding:kLineFragmentPadding];
[label_ setEditable:NO];
[label_ setSelectable:NO];
[label_ setDrawsBackground:NO];
base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
[[NSMutableParagraphStyle alloc] init]);
[paragraphStyle setLineSpacing:0.5 * [[label_ font] pointSize]];
[label_ setDefaultParagraphStyle:paragraphStyle];
inputField_.reset([[AutofillTextField alloc] initWithFrame:NSZeroRect]);
[inputField_ setHidden:YES];
spacer_.reset([[NSBox alloc] initWithFrame:NSZeroRect]);
[spacer_ setBoxType:NSBoxSeparator];
[spacer_ setBorderType:NSLineBorder];
base::scoped_nsobject<AutofillSuggestionView> view(
[[AutofillSuggestionView alloc] initWithFrame:NSZeroRect]);
[view setSubviews:
@[ label_, inputField_, spacer_ ]];
[view setInputField:inputField_];
[self setView:view];
}
- (void)setSuggestionText:(NSString*)line
icon:(NSImage*)icon
wrapText:(BOOL)wrapText {
[label_ setString:@""];
if ([icon size].width) {
base::scoped_nsobject<IconAttachmentCell> cell(
[[IconAttachmentCell alloc] initImageCell:icon]);
base::scoped_nsobject<NSTextAttachment> attachment(
[[NSTextAttachment alloc] init]);
[cell adjustBaselineForFont:[NSFont controlContentFontOfSize:0]];
[cell setAlignment:NSLeftTextAlignment];
[attachment setAttachmentCell:cell];
[[label_ textStorage] setAttributedString:
[NSAttributedString attributedStringWithAttachment:attachment]];
}
NSDictionary* attributes = @{
NSParagraphStyleAttributeName : [label_ defaultParagraphStyle],
NSCursorAttributeName : [NSCursor arrowCursor],
NSFontAttributeName : [NSFont controlContentFontOfSize:0]
};
base::scoped_nsobject<NSAttributedString> str1(
[[NSAttributedString alloc] initWithString:line
attributes:attributes]);
[[label_ textStorage] appendAttributedString:str1];
[label_ setVerticallyResizable:YES];
[label_ setHorizontallyResizable:!wrapText];
if (wrapText) {
CGFloat availableWidth =
4 * autofill::kFieldWidth - [inputField_ frame].size.width;
[label_ setFrameSize:NSMakeSize(availableWidth, kInfiniteSize)];
} else {
[label_ setFrameSize:NSMakeSize(kInfiniteSize, kInfiniteSize)];
}
[[label_ layoutManager] ensureLayoutForTextContainer:[label_ textContainer]];
[label_ sizeToFit];
}
- (void)
setSuggestionWithVerticallyCompactText:(NSString*)verticallyCompactText
horizontallyCompactText:(NSString*)horizontallyCompactText
icon:(NSImage*)icon
maxWidth:(CGFloat)maxWidth {
// Prefer the vertically compact text when it fits. If it doesn't fit, fall
// back to the horizontally compact text.
[self setSuggestionText:verticallyCompactText icon:icon wrapText:NO];
if ([self preferredSize].width > maxWidth)
[self setSuggestionText:horizontallyCompactText icon:icon wrapText:YES];
}
- (void)showInputField:(NSString*)text withIcon:(NSImage*)icon {
[[inputField_ cell] setPlaceholderString:text];
[[inputField_ cell] setIcon:icon];
[inputField_ setHidden:NO];
[inputField_ sizeToFit];
// Enforce fixed width.
NSSize frameSize = NSMakeSize(autofill::kFieldWidth,
NSHeight([inputField_ frame]));
[inputField_ setFrameSize:frameSize];
}
- (NSSize)preferredSize {
NSSize size = [label_ bounds].size;
// Final inputField_ sizing/spacing depends on a TODO(estade) in Views code.
if (![inputField_ isHidden]) {
size.height = std::max(size.height + kLabelWithInputTopPadding,
NSHeight([inputField_ frame]));
size.width += NSWidth([inputField_ frame]) + kAroundTextPadding;
}
size.height += kTopPadding;
return size;
}
- (void)performLayout {
NSRect bounds = [[self view] bounds];
NSSize preferredContainerSize = [self preferredSize];
// width is externally determined.
preferredContainerSize.width = NSWidth(bounds);
NSRect spacerFrame = NSMakeRect(0, preferredContainerSize.height - 1,
preferredContainerSize.width, 1);
NSRect labelFrame = [label_ bounds];
labelFrame.origin.x = NSMinX(bounds);
labelFrame.origin.y = NSMaxY(bounds) - NSHeight(labelFrame) - kTopPadding;
// Position input field - top is aligned to top of label field.
if (![inputField_ isHidden]) {
NSRect inputFieldFrame = [inputField_ frame];
inputFieldFrame.origin.x = NSMaxX(bounds) - NSWidth(inputFieldFrame);
inputFieldFrame.origin.y = NSMaxY(labelFrame) - NSHeight(inputFieldFrame);
[inputField_ setFrameOrigin:inputFieldFrame.origin];
// Vertically center the first line of the label with respect to the input
// field.
labelFrame.origin.y -= kLabelWithInputTopPadding;
// Due to fixed width, fields are guaranteed to not overlap.
DCHECK_LE(NSMaxX(labelFrame), NSMinX(inputFieldFrame));
}
[spacer_ setFrame:spacerFrame];
[label_ setFrame:labelFrame];
[[self view] setFrameSize:preferredContainerSize];
}
@end