summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/autofill/autofill_suggestion_container.mm
blob: fa4d39a1e88dc7ba0b54ed9bd7b17f084465d987 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// 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