1 // Copyright (c) 2012 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/browser/password_generation_bubble_controller.h"
7 #include "base/mac/foundation_util.h"
8 #include "base/strings/sys_string_conversions.h"
9 #include "chrome/browser/password_manager/password_manager.h"
10 #include "chrome/browser/ui/browser.h"
11 #include "chrome/browser/ui/browser_window.h"
12 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
13 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
14 #include "chrome/browser/ui/cocoa/key_equivalent_constants.h"
15 #import "chrome/browser/ui/cocoa/styled_text_field_cell.h"
16 #include "components/autofill/core/browser/password_generator.h"
17 #include "components/autofill/core/common/autofill_messages.h"
18 #include "components/autofill/core/common/password_form.h"
19 #include "components/autofill/core/common/password_generation_util.h"
20 #include "content/public/browser/render_view_host.h"
21 #include "grit/generated_resources.h"
22 #include "grit/theme_resources.h"
23 #import "ui/base/cocoa/tracking_area.h"
24 #include "ui/base/l10n/l10n_util_mac.h"
25 #include "ui/base/resource/resource_bundle.h"
29 // Size of the border in the bubble.
30 const CGFloat kBorderSize = 9.0;
32 // Visible size of the textfield.
33 const CGFloat kTextFieldHeight = 20.0;
34 const CGFloat kTextFieldWidth = 172.0;
36 // Frame padding necessary to make the textfield the correct visible size.
37 const CGFloat kTextFieldTopPadding = 3.0;
39 // Visible size of the button
40 const CGFloat kButtonWidth = 63.0;
41 const CGFloat kButtonHeight = 20.0;
43 // Padding that is added to the frame around the button to make it the
44 // correct visible size. Determined via visual inspection.
45 const CGFloat kButtonHorizontalPadding = 6.0;
46 const CGFloat kButtonVerticalPadding = 3.0;
48 // Visible size of the title.
49 const CGFloat kTitleWidth = 170.0;
50 const CGFloat kTitleHeight = 15.0;
52 // Space between the title and the textfield.
53 const CGFloat kVerticalSpacing = 13.0;
55 // Space between the textfield and the button.
56 const CGFloat kHorizontalSpacing = 7.0;
58 // We don't actually want the border to be kBorderSize on top as there is
59 // whitespace in the title text that makes it looks substantially bigger.
60 const CGFloat kTopBorderOffset = 3.0;
62 const CGFloat kIconSize = 26.0;
66 // Customized StyledTextFieldCell to display one button decoration that changes
68 @interface PasswordGenerationTextFieldCell : StyledTextFieldCell {
70 PasswordGenerationBubbleController* controller_;
72 base::scoped_nsobject<NSImage> normalImage_;
73 base::scoped_nsobject<NSImage> hoverImage_;
76 - (void)setUpWithController:(PasswordGenerationBubbleController*)controller
77 normalImage:(NSImage*)normalImage
78 hoverImage:(NSImage*)hoverImage;
79 - (void)mouseEntered:(NSEvent*)theEvent
80 inView:(PasswordGenerationTextField*)controlView;
81 - (void)mouseExited:(NSEvent*)theEvent
82 inView:(PasswordGenerationTextField*)controlView;
83 - (BOOL)mouseDown:(NSEvent*)theEvent
84 inView:(PasswordGenerationTextField*)controlView;
85 - (void)setUpTrackingAreaInRect:(NSRect)frame
86 ofView:(PasswordGenerationTextField*)controlView;
87 // Exposed for testing.
91 @implementation PasswordGenerationTextField
94 return [PasswordGenerationTextFieldCell class];
97 - (PasswordGenerationTextFieldCell*)cell {
98 return base::mac::ObjCCastStrict<PasswordGenerationTextFieldCell>(
102 - (id)initWithFrame:(NSRect)frame
103 withController:(PasswordGenerationBubbleController*)controller
104 normalImage:(NSImage*)normalImage
105 hoverImage:(NSImage*)hoverImage {
106 self = [super initWithFrame:frame];
108 PasswordGenerationTextFieldCell* cell = [self cell];
109 [cell setUpWithController:controller
110 normalImage:normalImage
111 hoverImage:hoverImage];
112 [cell setUpTrackingAreaInRect:[self bounds] ofView:self];
117 - (void)mouseEntered:(NSEvent*)theEvent {
118 [[self cell] mouseEntered:theEvent inView:self];
121 - (void)mouseExited:(NSEvent*)theEvent {
122 [[self cell] mouseExited:theEvent inView:self];
125 - (void)mouseDown:(NSEvent*)theEvent {
126 // Let the cell handle the click if it's in the decoration.
127 if (![[self cell] mouseDown:theEvent inView:self]) {
128 if ([self currentEditor]) {
129 [[self currentEditor] mouseDown:theEvent];
131 // We somehow lost focus.
132 [super mouseDown:theEvent];
137 - (void)simulateIconClick {
138 [[self cell] iconClicked];
143 @implementation PasswordGenerationTextFieldCell
145 - (void)setUpWithController:(PasswordGenerationBubbleController*)controller
146 normalImage:(NSImage*)normalImage
147 hoverImage:(NSImage*)hoverImage {
148 controller_ = controller;
150 normalImage_.reset([normalImage retain]);
151 hoverImage_.reset([hoverImage retain]);
152 [self setLineBreakMode:NSLineBreakByTruncatingTail];
153 [self setTruncatesLastVisibleLine:YES];
156 - (void)splitFrame:(NSRect*)cellFrame toIconFrame:(NSRect*)iconFrame {
157 NSDivideRect(*cellFrame, iconFrame, cellFrame,
158 kIconSize, NSMaxXEdge);
161 - (NSRect)getIconFrame:(NSRect)cellFrame {
163 [self splitFrame:&cellFrame toIconFrame:&iconFrame];
167 - (NSRect)getTextFrame:(NSRect)cellFrame {
169 [self splitFrame:&cellFrame toIconFrame:&iconFrame];
173 - (BOOL)eventIsInDecoration:(NSEvent*)theEvent
174 inView:(PasswordGenerationTextField*)controlView {
175 NSPoint mouseLocation = [controlView convertPoint:[theEvent locationInWindow]
177 NSRect cellFrame = [controlView bounds];
178 return NSMouseInRect(mouseLocation,
179 [self getIconFrame:cellFrame],
180 [controlView isFlipped]);
183 - (void)mouseEntered:(NSEvent*)theEvent
184 inView:(PasswordGenerationTextField*)controlView {
186 [controlView setNeedsDisplay:YES];
189 - (void)mouseExited:(NSEvent*)theEvent
190 inView:(PasswordGenerationTextField*)controlView {
192 [controlView setNeedsDisplay:YES];
195 - (BOOL)mouseDown:(NSEvent*)theEvent
196 inView:(PasswordGenerationTextField*)controlView {
197 if ([self eventIsInDecoration:theEvent inView:controlView]) {
204 - (void)iconClicked {
205 [controller_ regeneratePassword];
208 - (NSImage*)getImage {
214 - (NSRect)adjustFrameForFrame:(NSRect)frame {
215 // By default, there appears to be a 2 pixel gap between what is considered
216 // part of the textFrame and what is considered part of the icon.
217 // TODO(gcasto): This really should be fixed in StyledTextFieldCell, as it
218 // looks like the location bar also suffers from this issue.
219 frame.size.width += 2;
223 - (NSRect)textFrameForFrame:(NSRect)cellFrame {
224 // Baseclass insets the rect by top and bottom offsets.
225 NSRect textFrame = [super textFrameForFrame:cellFrame];
226 textFrame = [self getTextFrame:textFrame];
227 return [self adjustFrameForFrame:textFrame];
230 - (NSRect)textCursorFrameForFrame:(NSRect)cellFrame {
231 NSRect textFrame = [self getTextFrame:cellFrame];
232 return [self adjustFrameForFrame:textFrame];
235 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
236 NSImage* image = [self getImage];
237 NSRect iconFrame = [self getIconFrame:cellFrame];
238 // Center the image in the available space. At the moment the image is
239 // slightly larger than the frame so we crop it.
240 // Offset the full difference on the left hand side since the border on the
241 // right takes up some space. Offset half the vertical difference on the
242 // bottom so that the image stays vertically centered.
243 const CGFloat xOffset = [image size].width - NSWidth(iconFrame);
244 const CGFloat yOffset = ([image size].height - (NSHeight(iconFrame))) / 2.0;
245 NSRect croppedRect = NSMakeRect(xOffset,
248 NSHeight(iconFrame));
250 [image drawInRect:iconFrame
252 operation:NSCompositeSourceOver
257 [super drawInteriorWithFrame:cellFrame inView:controlView];
260 - (void)setUpTrackingAreaInRect:(NSRect)frame
261 ofView:(PasswordGenerationTextField*)view {
262 NSRect iconFrame = [self getIconFrame:frame];
263 base::scoped_nsobject<CrTrackingArea> area(
264 [[CrTrackingArea alloc] initWithRect:iconFrame
265 options:NSTrackingMouseEnteredAndExited |
266 NSTrackingActiveAlways owner:view userInfo:nil]);
267 [view addTrackingArea:area];
270 - (CGFloat)topTextFrameOffset {
274 - (CGFloat)bottomTextFrameOffset {
278 - (CGFloat)cornerRadius {
282 - (BOOL)shouldDrawBezel {
288 @implementation PasswordGenerationBubbleController
290 @synthesize textField = textField_;
292 - (id)initWithWindow:(NSWindow*)parentWindow
293 anchoredAt:(NSPoint)point
294 renderViewHost:(content::RenderViewHost*)renderViewHost
295 passwordManager:(PasswordManager*)passwordManager
296 usingGenerator:(autofill::PasswordGenerator*)passwordGenerator
297 forForm:(const autofill::PasswordForm&)form {
298 CGFloat width = (kBorderSize*2 +
302 CGFloat height = (kBorderSize*2 +
307 info_bubble::kBubbleArrowHeight);
308 NSRect contentRect = NSMakeRect(0, 0, width, height);
309 base::scoped_nsobject<InfoBubbleWindow> window(
310 [[InfoBubbleWindow alloc] initWithContentRect:contentRect
311 styleMask:NSBorderlessWindowMask
312 backing:NSBackingStoreBuffered
314 if (self = [super initWithWindow:window
315 parentWindow:parentWindow
317 passwordGenerator_ = passwordGenerator;
318 renderViewHost_ = renderViewHost;
319 passwordManager_ = passwordManager;
321 [[self bubble] setArrowLocation:info_bubble::kTopLeft];
322 [self performLayout];
328 - (void)performLayout {
329 NSView* contentView = [[self window] contentView];
330 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
332 textField_ = [[[PasswordGenerationTextField alloc]
333 initWithFrame:NSMakeRect(kBorderSize,
336 kTextFieldHeight + kTextFieldTopPadding)
338 normalImage:rb.GetNativeImageNamed(IDR_RELOAD_DIMMED).ToNSImage()
339 hoverImage:rb.GetNativeImageNamed(IDR_RELOAD)
340 .ToNSImage()] autorelease];
341 gfx::Font smallBoldFont =
342 rb.GetFont(ResourceBundle::SmallFont).DeriveFont(0, gfx::Font::BOLD);
343 [textField_ setFont:smallBoldFont.GetNativeFont()];
345 setStringValue:base::SysUTF8ToNSString(passwordGenerator_->Generate())];
346 [textField_ setDelegate:self];
347 [contentView addSubview:textField_];
349 CGFloat buttonX = (NSMaxX([textField_ frame]) +
351 kButtonHorizontalPadding);
352 CGFloat buttonY = kBorderSize - kButtonVerticalPadding;
354 [[NSButton alloc] initWithFrame:NSMakeRect(
357 kButtonWidth + 2 * kButtonHorizontalPadding,
358 kButtonHeight + 2 * kButtonVerticalPadding)];
359 [button setBezelStyle:NSRoundedBezelStyle];
360 [button setTitle:l10n_util::GetNSString(IDS_PASSWORD_GENERATION_BUTTON_TEXT)];
361 [button setTarget:self];
362 [button setAction:@selector(fillPassword:)];
363 [contentView addSubview:button];
365 base::scoped_nsobject<NSTextField> title([[NSTextField alloc] initWithFrame:
366 NSMakeRect(kBorderSize,
367 kBorderSize + kTextFieldHeight + kVerticalSpacing,
370 [title setEditable:NO];
371 [title setBordered:NO];
372 [title setStringValue:l10n_util::GetNSString(
373 IDS_PASSWORD_GENERATION_BUBBLE_TITLE)];
374 [contentView addSubview:title];
377 - (IBAction)fillPassword:(id)sender {
378 if (renderViewHost_) {
379 renderViewHost_->Send(
380 new AutofillMsg_GeneratedPasswordAccepted(
381 renderViewHost_->GetRoutingID(),
382 base::SysNSStringToUTF16([textField_ stringValue])));
384 if (passwordManager_)
385 passwordManager_->SetFormHasGeneratedPassword(form_);
387 actions_.password_accepted = true;
391 - (void)regeneratePassword {
393 setStringValue:base::SysUTF8ToNSString(passwordGenerator_->Generate())];
394 actions_.password_regenerated = true;
397 - (void)controlTextDidChange:(NSNotification*)notification {
398 actions_.password_edited = true;
401 - (void)windowWillClose:(NSNotification*)notification {
402 autofill::password_generation::LogUserActions(actions_);
403 [super windowWillClose:notification];