// 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