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. 4 5#import "chrome/browser/ui/cocoa/browser/password_generation_bubble_controller.h" 6 7#include "base/mac/foundation_util.h" 8#include "base/strings/sys_string_conversions.h" 9#include "chrome/browser/ui/browser.h" 10#include "chrome/browser/ui/browser_window.h" 11#import "chrome/browser/ui/cocoa/info_bubble_view.h" 12#import "chrome/browser/ui/cocoa/info_bubble_window.h" 13#include "chrome/browser/ui/cocoa/key_equivalent_constants.h" 14#import "chrome/browser/ui/cocoa/styled_text_field_cell.h" 15#include "components/autofill/content/common/autofill_messages.h" 16#include "components/autofill/core/browser/password_generator.h" 17#include "components/autofill/core/common/password_form.h" 18#include "components/autofill/core/common/password_generation_util.h" 19#include "components/password_manager/core/browser/password_manager.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" 26#include "ui/gfx/font_list.h" 27 28namespace { 29 30// Size of the border in the bubble. 31const CGFloat kBorderSize = 9.0; 32 33// Visible size of the textfield. 34const CGFloat kTextFieldHeight = 20.0; 35const CGFloat kTextFieldWidth = 172.0; 36 37// Frame padding necessary to make the textfield the correct visible size. 38const CGFloat kTextFieldTopPadding = 3.0; 39 40// Visible size of the button 41const CGFloat kButtonWidth = 63.0; 42const CGFloat kButtonHeight = 20.0; 43 44// Padding that is added to the frame around the button to make it the 45// correct visible size. Determined via visual inspection. 46const CGFloat kButtonHorizontalPadding = 6.0; 47const CGFloat kButtonVerticalPadding = 3.0; 48 49// Visible size of the title. 50const CGFloat kTitleWidth = 170.0; 51const CGFloat kTitleHeight = 15.0; 52 53// Space between the title and the textfield. 54const CGFloat kVerticalSpacing = 13.0; 55 56// Space between the textfield and the button. 57const CGFloat kHorizontalSpacing = 7.0; 58 59// We don't actually want the border to be kBorderSize on top as there is 60// whitespace in the title text that makes it looks substantially bigger. 61const CGFloat kTopBorderOffset = 3.0; 62 63const CGFloat kIconSize = 26.0; 64 65} // namespace 66 67// Customized StyledTextFieldCell to display one button decoration that changes 68// on hover. 69@interface PasswordGenerationTextFieldCell : StyledTextFieldCell { 70 @private 71 PasswordGenerationBubbleController* controller_; 72 BOOL hovering_; 73 base::scoped_nsobject<NSImage> normalImage_; 74 base::scoped_nsobject<NSImage> hoverImage_; 75} 76 77- (void)setUpWithController:(PasswordGenerationBubbleController*)controller 78 normalImage:(NSImage*)normalImage 79 hoverImage:(NSImage*)hoverImage; 80- (void)mouseEntered:(NSEvent*)theEvent 81 inView:(PasswordGenerationTextField*)controlView; 82- (void)mouseExited:(NSEvent*)theEvent 83 inView:(PasswordGenerationTextField*)controlView; 84- (BOOL)mouseDown:(NSEvent*)theEvent 85 inView:(PasswordGenerationTextField*)controlView; 86- (void)setUpTrackingAreaInRect:(NSRect)frame 87 ofView:(PasswordGenerationTextField*)controlView; 88// Exposed for testing. 89- (void)iconClicked; 90@end 91 92@implementation PasswordGenerationTextField 93 94+ (Class)cellClass { 95 return [PasswordGenerationTextFieldCell class]; 96} 97 98- (PasswordGenerationTextFieldCell*)cell { 99 return base::mac::ObjCCastStrict<PasswordGenerationTextFieldCell>( 100 [super cell]); 101} 102 103- (id)initWithFrame:(NSRect)frame 104 withController:(PasswordGenerationBubbleController*)controller 105 normalImage:(NSImage*)normalImage 106 hoverImage:(NSImage*)hoverImage { 107 self = [super initWithFrame:frame]; 108 if (self) { 109 PasswordGenerationTextFieldCell* cell = [self cell]; 110 [cell setUpWithController:controller 111 normalImage:normalImage 112 hoverImage:hoverImage]; 113 [cell setUpTrackingAreaInRect:[self bounds] ofView:self]; 114 } 115 return self; 116} 117 118- (void)mouseEntered:(NSEvent*)theEvent { 119 [[self cell] mouseEntered:theEvent inView:self]; 120} 121 122- (void)mouseExited:(NSEvent*)theEvent { 123 [[self cell] mouseExited:theEvent inView:self]; 124} 125 126- (void)mouseDown:(NSEvent*)theEvent { 127 // Let the cell handle the click if it's in the decoration. 128 if (![[self cell] mouseDown:theEvent inView:self]) { 129 if ([self currentEditor]) { 130 [[self currentEditor] mouseDown:theEvent]; 131 } else { 132 // We somehow lost focus. 133 [super mouseDown:theEvent]; 134 } 135 } 136} 137 138- (void)simulateIconClick { 139 [[self cell] iconClicked]; 140} 141 142@end 143 144@implementation PasswordGenerationTextFieldCell 145 146- (void)setUpWithController:(PasswordGenerationBubbleController*)controller 147 normalImage:(NSImage*)normalImage 148 hoverImage:(NSImage*)hoverImage { 149 controller_ = controller; 150 hovering_ = NO; 151 normalImage_.reset([normalImage retain]); 152 hoverImage_.reset([hoverImage retain]); 153 [self setLineBreakMode:NSLineBreakByTruncatingTail]; 154 [self setTruncatesLastVisibleLine:YES]; 155} 156 157- (void)splitFrame:(NSRect*)cellFrame toIconFrame:(NSRect*)iconFrame { 158 NSDivideRect(*cellFrame, iconFrame, cellFrame, 159 kIconSize, NSMaxXEdge); 160} 161 162- (NSRect)getIconFrame:(NSRect)cellFrame { 163 NSRect iconFrame; 164 [self splitFrame:&cellFrame toIconFrame:&iconFrame]; 165 return iconFrame; 166} 167 168- (NSRect)getTextFrame:(NSRect)cellFrame { 169 NSRect iconFrame; 170 [self splitFrame:&cellFrame toIconFrame:&iconFrame]; 171 return cellFrame; 172} 173 174- (BOOL)eventIsInDecoration:(NSEvent*)theEvent 175 inView:(PasswordGenerationTextField*)controlView { 176 NSPoint mouseLocation = [controlView convertPoint:[theEvent locationInWindow] 177 fromView:nil]; 178 NSRect cellFrame = [controlView bounds]; 179 return NSMouseInRect(mouseLocation, 180 [self getIconFrame:cellFrame], 181 [controlView isFlipped]); 182} 183 184- (void)mouseEntered:(NSEvent*)theEvent 185 inView:(PasswordGenerationTextField*)controlView { 186 hovering_ = YES; 187 [controlView setNeedsDisplay:YES]; 188} 189 190- (void)mouseExited:(NSEvent*)theEvent 191 inView:(PasswordGenerationTextField*)controlView { 192 hovering_ = NO; 193 [controlView setNeedsDisplay:YES]; 194} 195 196- (BOOL)mouseDown:(NSEvent*)theEvent 197 inView:(PasswordGenerationTextField*)controlView { 198 if ([self eventIsInDecoration:theEvent inView:controlView]) { 199 [self iconClicked]; 200 return YES; 201 } 202 return NO; 203} 204 205- (void)iconClicked { 206 [controller_ regeneratePassword]; 207} 208 209- (NSImage*)getImage { 210 if (hovering_) 211 return hoverImage_; 212 return normalImage_; 213} 214 215- (NSRect)adjustFrameForFrame:(NSRect)frame { 216 // By default, there appears to be a 2 pixel gap between what is considered 217 // part of the textFrame and what is considered part of the icon. 218 // TODO(gcasto): This really should be fixed in StyledTextFieldCell, as it 219 // looks like the location bar also suffers from this issue. 220 frame.size.width += 2; 221 return frame; 222} 223 224- (NSRect)textFrameForFrame:(NSRect)cellFrame { 225 // Baseclass insets the rect by top and bottom offsets. 226 NSRect textFrame = [super textFrameForFrame:cellFrame]; 227 textFrame = [self getTextFrame:textFrame]; 228 return [self adjustFrameForFrame:textFrame]; 229} 230 231- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame { 232 NSRect textFrame = [self getTextFrame:cellFrame]; 233 return [self adjustFrameForFrame:textFrame]; 234} 235 236- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView { 237 NSImage* image = [self getImage]; 238 NSRect iconFrame = [self getIconFrame:cellFrame]; 239 // Center the image in the available space. At the moment the image is 240 // slightly larger than the frame so we crop it. 241 // Offset the full difference on the left hand side since the border on the 242 // right takes up some space. Offset half the vertical difference on the 243 // bottom so that the image stays vertically centered. 244 const CGFloat xOffset = [image size].width - NSWidth(iconFrame); 245 const CGFloat yOffset = ([image size].height - (NSHeight(iconFrame))) / 2.0; 246 NSRect croppedRect = NSMakeRect(xOffset, 247 yOffset, 248 NSWidth(iconFrame), 249 NSHeight(iconFrame)); 250 251 [image drawInRect:iconFrame 252 fromRect:croppedRect 253 operation:NSCompositeSourceOver 254 fraction:1.0 255 respectFlipped:YES 256 hints:nil]; 257 258 [super drawInteriorWithFrame:cellFrame inView:controlView]; 259} 260 261- (void)setUpTrackingAreaInRect:(NSRect)frame 262 ofView:(PasswordGenerationTextField*)view { 263 NSRect iconFrame = [self getIconFrame:frame]; 264 base::scoped_nsobject<CrTrackingArea> area( 265 [[CrTrackingArea alloc] initWithRect:iconFrame 266 options:NSTrackingMouseEnteredAndExited | 267 NSTrackingActiveAlways owner:view userInfo:nil]); 268 [view addTrackingArea:area]; 269} 270 271- (CGFloat)topTextFrameOffset { 272 return 1.0; 273} 274 275- (CGFloat)bottomTextFrameOffset { 276 return 1.0; 277} 278 279- (CGFloat)cornerRadius { 280 return 4.0; 281} 282 283- (BOOL)shouldDrawBezel { 284 return YES; 285} 286 287@end 288 289@implementation PasswordGenerationBubbleController 290 291@synthesize textField = textField_; 292 293- (id)initWithWindow:(NSWindow*)parentWindow 294 anchoredAt:(NSPoint)point 295 renderViewHost:(content::RenderViewHost*)renderViewHost 296 passwordManager:(password_manager::PasswordManager*)passwordManager 297 usingGenerator:(autofill::PasswordGenerator*)passwordGenerator 298 forForm:(const autofill::PasswordForm&)form { 299 CGFloat width = (kBorderSize*2 + 300 kTextFieldWidth + 301 kHorizontalSpacing + 302 kButtonWidth); 303 CGFloat height = (kBorderSize*2 + 304 kTextFieldHeight + 305 kVerticalSpacing + 306 kTitleHeight - 307 kTopBorderOffset + 308 info_bubble::kBubbleArrowHeight); 309 NSRect contentRect = NSMakeRect(0, 0, width, height); 310 base::scoped_nsobject<InfoBubbleWindow> window( 311 [[InfoBubbleWindow alloc] initWithContentRect:contentRect 312 styleMask:NSBorderlessWindowMask 313 backing:NSBackingStoreBuffered 314 defer:NO]); 315 if (self = [super initWithWindow:window 316 parentWindow:parentWindow 317 anchoredAt:point]) { 318 passwordGenerator_ = passwordGenerator; 319 renderViewHost_ = renderViewHost; 320 passwordManager_ = passwordManager; 321 form_ = form; 322 [[self bubble] setArrowLocation:info_bubble::kTopLeft]; 323 [self performLayout]; 324 } 325 326 return self; 327} 328 329- (void)performLayout { 330 NSView* contentView = [[self window] contentView]; 331 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 332 333 textField_ = [[[PasswordGenerationTextField alloc] 334 initWithFrame:NSMakeRect(kBorderSize, 335 kBorderSize, 336 kTextFieldWidth, 337 kTextFieldHeight + kTextFieldTopPadding) 338 withController:self 339 normalImage:rb.GetNativeImageNamed(IDR_RELOAD_DIMMED).ToNSImage() 340 hoverImage:rb.GetNativeImageNamed(IDR_RELOAD) 341 .ToNSImage()] autorelease]; 342 const gfx::FontList& smallBoldFont = 343 rb.GetFontList(ui::ResourceBundle::SmallBoldFont); 344 [textField_ setFont:smallBoldFont.GetPrimaryFont().GetNativeFont()]; 345 [textField_ 346 setStringValue:base::SysUTF8ToNSString(passwordGenerator_->Generate())]; 347 [textField_ setDelegate:self]; 348 [contentView addSubview:textField_]; 349 350 CGFloat buttonX = (NSMaxX([textField_ frame]) + 351 kHorizontalSpacing - 352 kButtonHorizontalPadding); 353 CGFloat buttonY = kBorderSize - kButtonVerticalPadding; 354 NSButton* button = 355 [[NSButton alloc] initWithFrame:NSMakeRect( 356 buttonX, 357 buttonY, 358 kButtonWidth + 2 * kButtonHorizontalPadding, 359 kButtonHeight + 2 * kButtonVerticalPadding)]; 360 [button setBezelStyle:NSRoundedBezelStyle]; 361 [button setTitle:l10n_util::GetNSString(IDS_PASSWORD_GENERATION_BUTTON_TEXT)]; 362 [button setTarget:self]; 363 [button setAction:@selector(fillPassword:)]; 364 [contentView addSubview:button]; 365 366 base::scoped_nsobject<NSTextField> title([[NSTextField alloc] initWithFrame: 367 NSMakeRect(kBorderSize, 368 kBorderSize + kTextFieldHeight + kVerticalSpacing, 369 kTitleWidth, 370 kTitleHeight)]); 371 [title setEditable:NO]; 372 [title setBordered:NO]; 373 [title setStringValue:l10n_util::GetNSString( 374 IDS_PASSWORD_GENERATION_BUBBLE_TITLE)]; 375 [contentView addSubview:title]; 376} 377 378- (IBAction)fillPassword:(id)sender { 379 if (renderViewHost_) { 380 renderViewHost_->Send( 381 new AutofillMsg_GeneratedPasswordAccepted( 382 renderViewHost_->GetRoutingID(), 383 base::SysNSStringToUTF16([textField_ stringValue]))); 384 } 385 if (passwordManager_) 386 passwordManager_->SetFormHasGeneratedPassword(form_); 387 388 actions_.password_accepted = true; 389 [self close]; 390} 391 392- (void)regeneratePassword { 393 [textField_ 394 setStringValue:base::SysUTF8ToNSString(passwordGenerator_->Generate())]; 395 actions_.password_regenerated = true; 396} 397 398- (void)controlTextDidChange:(NSNotification*)notification { 399 actions_.password_edited = true; 400} 401 402- (void)windowWillClose:(NSNotification*)notification { 403 autofill::password_generation::LogUserActions(actions_); 404 [super windowWillClose:notification]; 405} 406 407@end 408