1 // Copyright 2014 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/password_generation_popup_view_cocoa.h"
9 #include "base/logging.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/browser/ui/autofill/autofill_popup_controller.h"
12 #include "chrome/browser/ui/autofill/autofill_popup_view.h"
13 #include "chrome/browser/ui/autofill/popup_constants.h"
14 #include "chrome/browser/ui/chrome_style.h"
15 #include "chrome/browser/ui/cocoa/autofill/password_generation_popup_view_bridge.h"
16 #import "chrome/browser/ui/cocoa/l10n_util.h"
17 #include "components/autofill/core/browser/popup_item_ids.h"
18 #include "grit/theme_resources.h"
19 #include "skia/ext/skia_utils_mac.h"
20 #import "ui/base/cocoa/controls/hyperlink_text_view.h"
21 #include "ui/base/resource/resource_bundle.h"
22 #include "ui/gfx/font_list.h"
23 #include "ui/gfx/geometry/point.h"
24 #include "ui/gfx/geometry/rect.h"
25 #include "ui/gfx/image/image.h"
26 #include "ui/gfx/range/range.h"
27 #include "ui/gfx/text_constants.h"
29 using autofill::AutofillPopupView;
30 using autofill::PasswordGenerationPopupController;
31 using autofill::PasswordGenerationPopupView;
32 using base::scoped_nsobject;
36 // The height of the divider between the password and help sections, in pixels.
37 const CGFloat kDividerHeight = 1;
39 // The amount of whitespace, in pixels, between lines of text in the password
41 const CGFloat kPasswordSectionVerticalSeparation = 5;
43 NSColor* DividerColor() {
44 return gfx::SkColorToCalibratedNSColor(
45 PasswordGenerationPopupView::kDividerColor);
48 NSColor* HelpTextBackgroundColor() {
49 return gfx::SkColorToCalibratedNSColor(
50 PasswordGenerationPopupView::kExplanatoryTextBackgroundColor);
53 NSColor* HelpTextColor() {
54 return gfx::SkColorToCalibratedNSColor(
55 PasswordGenerationPopupView::kExplanatoryTextColor);
58 NSColor* HelpLinkColor() {
59 return gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor());
64 @implementation PasswordGenerationPopupViewCocoa
66 #pragma mark Initialisers
68 - (id)initWithFrame:(NSRect)frame {
73 - (id)initWithController:
74 (autofill::PasswordGenerationPopupController*)controller
76 if (self = [super initWithDelegate:controller frame:frame]) {
77 controller_ = controller;
79 passwordSection_.reset([[NSView alloc] initWithFrame:NSZeroRect]);
80 [self addSubview:passwordSection_];
83 [[self textFieldWithText:controller_->password()
84 attributes:[self passwordAttributes]] retain]);
85 [passwordSection_ addSubview:passwordField_];
87 passwordTitleField_.reset(
88 [[self textFieldWithText:controller_->SuggestedText()
89 attributes:[self passwordTitleAttributes]] retain]);
90 [passwordSection_ addSubview:passwordTitleField_];
92 keyIcon_.reset([[NSImageView alloc] initWithFrame:NSZeroRect]);
93 NSImage* keyImage = ResourceBundle::GetSharedInstance()
94 .GetImageNamed(IDR_GENERATE_PASSWORD_KEY)
96 [keyIcon_ setImage:keyImage];
97 [passwordSection_ addSubview:keyIcon_];
99 divider_.reset([[NSBox alloc] initWithFrame:NSZeroRect]);
100 [divider_ setBoxType:NSBoxCustom];
101 [divider_ setBorderType:NSLineBorder];
102 [divider_ setBorderColor:DividerColor()];
103 [self addSubview:divider_];
105 helpTextView_.reset([[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
106 [helpTextView_ setMessage:base::SysUTF16ToNSString(controller_->HelpText())
107 withFont:[self textFont]
108 messageColor:HelpTextColor()];
109 [helpTextView_ addLinkRange:controller_->HelpTextLinkRange().ToNSRange()
111 linkColor:HelpLinkColor()];
112 [helpTextView_ setDelegate:self];
113 [helpTextView_ setDrawsBackground:YES];
114 [helpTextView_ setBackgroundColor:HelpTextBackgroundColor()];
116 setTextContainerInset:NSMakeSize(controller_->kHorizontalPadding,
117 controller_->kHelpVerticalPadding)];
118 // Remove the underlining.
119 NSTextStorage* text = [helpTextView_ textStorage];
120 [text addAttribute:NSUnderlineStyleAttributeName
121 value:@(NSUnderlineStyleNone)
122 range:controller_->HelpTextLinkRange().ToNSRange()];
123 [self addSubview:helpTextView_];
129 - (void)updateTrackingAreas {
130 [super updateTrackingAreas];
131 if (helpTextTrackingArea_.get())
132 [self removeTrackingArea:helpTextTrackingArea_.get()];
134 // Set up tracking for the help text so the cursor, etc. is properly handled.
135 // Must set tracking to "always" because the autofill window is never key.
136 NSTrackingAreaOptions options = NSTrackingActiveAlways |
137 NSTrackingMouseEnteredAndExited |
138 NSTrackingMouseMoved |
139 NSTrackingCursorUpdate;
140 helpTextTrackingArea_.reset(
141 [[CrTrackingArea alloc] initWithRect:[self bounds]
143 owner:helpTextView_.get()
146 [self addTrackingArea:helpTextTrackingArea_.get()];
149 #pragma mark NSView implementation:
151 - (void)drawRect:(NSRect)dirtyRect {
152 [super drawRect:dirtyRect];
154 // If the view is in the process of being destroyed, don't bother drawing.
158 [self drawBackgroundAndBorder];
160 if (controller_->password_selected()) {
161 // Draw a highlight under the suggested password.
162 NSRect highlightBounds = [passwordSection_ frame];
163 [[self highlightColor] set];
164 [NSBezierPath fillRect:highlightBounds];
168 #pragma mark Public API:
170 - (NSSize)preferredSize {
171 const NSSize passwordTitleSize =
172 [base::SysUTF16ToNSString(controller_->SuggestedText())
173 sizeWithAttributes:@{ NSFontAttributeName : [self boldFont] }];
174 const NSSize passwordSize = [base::SysUTF16ToNSString(controller_->password())
175 sizeWithAttributes:@{ NSFontAttributeName : [self textFont] }];
178 autofill::kPopupBorderThickness +
179 controller_->kHorizontalPadding +
180 [[keyIcon_ image] size].width +
181 controller_->kHorizontalPadding +
182 std::max(passwordSize.width, passwordTitleSize.width) +
183 controller_->kHorizontalPadding +
184 autofill::kPopupBorderThickness;
186 width = std::max(width, (CGFloat)controller_->GetMinimumWidth());
187 CGFloat contentWidth = width - (2 * controller_->kHorizontalPadding);
190 autofill::kPopupBorderThickness +
191 controller_->kHelpVerticalPadding +
192 [self helpSizeForPopupWidth:contentWidth].height +
193 controller_->kHelpVerticalPadding +
194 autofill::kPopupBorderThickness;
196 if (controller_->display_password())
197 height += controller_->kPopupPasswordSectionHeight;
199 return NSMakeSize(width, height);
202 - (void)updateBoundsAndRedrawPopup {
203 const CGFloat popupWidth = controller_->popup_bounds().width();
204 const CGFloat contentWidth =
205 popupWidth - (2 * autofill::kPopupBorderThickness);
206 const CGFloat contentHeight = controller_->popup_bounds().height() -
207 (2 * autofill::kPopupBorderThickness);
209 if (controller_->display_password()) {
210 // The password can change while the bubble is shown: If the user has
211 // accepted the password and then selects the form again and starts deleting
212 // the password, the field will be initially invisible and then become
214 [self updatePassword];
216 // Lay out the password section, which includes the key icon, the title, and
217 // the suggested password.
219 setFrame:NSMakeRect(autofill::kPopupBorderThickness,
220 autofill::kPopupBorderThickness,
222 controller_->kPopupPasswordSectionHeight)];
224 // The key icon falls to the left of the title and password.
225 const NSSize imageSize = [[keyIcon_ image] size];
226 const CGFloat keyX = controller_->kHorizontalPadding;
228 std::ceil((controller_->kPopupPasswordSectionHeight / 2.0) -
229 (imageSize.height / 2.0));
230 [keyIcon_ setFrame:{ NSMakePoint(keyX, keyY), imageSize }];
232 // The title and password fall to the right of the key icon and are centered
233 // vertically as a group with some padding in between.
234 [passwordTitleField_ sizeToFit];
235 [passwordField_ sizeToFit];
236 const CGFloat groupHeight = NSHeight([passwordField_ frame]) +
237 kPasswordSectionVerticalSeparation +
238 NSHeight([passwordTitleField_ frame]);
239 const CGFloat groupX =
240 NSMaxX([keyIcon_ frame]) + controller_->kHorizontalPadding;
241 const CGFloat groupY =
242 std::ceil((controller_->kPopupPasswordSectionHeight / 2.0) -
243 (groupHeight / 2.0));
244 [passwordField_ setFrameOrigin:NSMakePoint(groupX, groupY)];
245 const CGFloat titleY = groupY +
246 NSHeight([passwordField_ frame]) +
247 kPasswordSectionVerticalSeparation;
248 [passwordTitleField_ setFrameOrigin:NSMakePoint(groupX, titleY)];
250 // Layout the divider, which falls immediately below the password section.
251 const CGFloat dividerX = autofill::kPopupBorderThickness;
252 const CGFloat dividerY = NSMaxY([passwordSection_ frame]);
253 NSRect dividerFrame =
254 NSMakeRect(dividerX, dividerY, contentWidth, kDividerHeight);
255 [divider_ setFrame:dividerFrame];
258 // Layout the help section beneath the divider (if applicable, otherwise
259 // beneath the border).
260 const CGFloat helpX = autofill::kPopupBorderThickness;
261 const CGFloat helpY = controller_->display_password()
262 ? NSMaxY([divider_ frame])
263 : autofill::kPopupBorderThickness;
264 const CGFloat helpHeight = contentHeight -
265 NSHeight([passwordSection_ frame]) -
266 NSHeight([divider_ frame]);
267 [helpTextView_ setFrame:NSMakeRect(helpX, helpY, contentWidth, helpHeight)];
269 [super updateBoundsAndRedrawPopup];
272 - (BOOL)isPointInPasswordBounds:(NSPoint)point {
273 return NSPointInRect(point, [passwordSection_ frame]);
276 - (void)controllerDestroyed {
278 [super delegateDestroyed];
281 #pragma mark NSTextViewDelegate implementation:
283 - (BOOL)textView:(NSTextView*)textView
284 clickedOnLink:(id)link
285 atIndex:(NSUInteger)charIndex {
286 controller_->OnSavedPasswordsLinkClicked();
290 #pragma mark Private helpers:
292 - (void)updatePassword {
293 base::scoped_nsobject<NSMutableAttributedString> updatedPassword(
294 [[NSMutableAttributedString alloc]
295 initWithString:base::SysUTF16ToNSString(controller_->password())
296 attributes:[self passwordAttributes]]);
297 [passwordField_ setAttributedStringValue:updatedPassword];
300 - (NSDictionary*)passwordTitleAttributes {
301 scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
302 [[NSMutableParagraphStyle alloc] init]);
303 [paragraphStyle setAlignment:NSLeftTextAlignment];
305 NSFontAttributeName : [self boldFont],
306 NSForegroundColorAttributeName : [self nameColor],
307 NSParagraphStyleAttributeName : paragraphStyle.autorelease()
311 - (NSDictionary*)passwordAttributes {
312 scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
313 [[NSMutableParagraphStyle alloc] init]);
314 [paragraphStyle setAlignment:NSLeftTextAlignment];
316 NSFontAttributeName : [self textFont],
317 NSForegroundColorAttributeName : [self nameColor],
318 NSParagraphStyleAttributeName : paragraphStyle.autorelease()
322 - (NSTextField*)textFieldWithText:(const base::string16&)text
323 attributes:(NSDictionary*)attributes {
324 NSTextField* textField =
325 [[[NSTextField alloc] initWithFrame:NSZeroRect] autorelease];
326 scoped_nsobject<NSAttributedString> attributedString(
327 [[NSAttributedString alloc]
328 initWithString:base::SysUTF16ToNSString(text)
329 attributes:attributes]);
330 [textField setAttributedStringValue:attributedString.autorelease()];
331 [textField setEditable:NO];
332 [textField setSelectable:NO];
333 [textField setDrawsBackground:NO];
334 [textField setBezeled:NO];
338 - (NSSize)helpSizeForPopupWidth:(CGFloat)width {
339 const CGFloat helpWidth = width -
340 2 * controller_->kHorizontalPadding -
341 2 * autofill::kPopupBorderThickness;
342 const NSSize size = NSMakeSize(helpWidth, MAXFLOAT);
343 NSRect textFrame = [base::SysUTF16ToNSString(controller_->HelpText())
344 boundingRectWithSize:size
345 options:NSLineBreakByWordWrapping |
346 NSStringDrawingUsesLineFragmentOrigin
347 attributes:@{ NSFontAttributeName : [self textFont] }];
348 return textFrame.size;
351 - (NSFont*)boldFont {
352 return [NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]];
355 - (NSFont*)textFont {
356 return [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];