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/passwords/manage_password_item_view_controller.h"
7 #include "base/logging.h"
8 #include "base/strings/string16.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "chrome/browser/ui/chrome_style.h"
11 #import "chrome/browser/ui/cocoa/passwords/manage_passwords_bubble_content_view_controller.h"
12 #include "chrome/browser/ui/passwords/manage_passwords_bubble_model.h"
13 #include "grit/generated_resources.h"
14 #include "skia/ext/skia_utils_mac.h"
15 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
16 #import "ui/base/cocoa/hover_image_button.h"
17 #include "ui/base/l10n/l10n_util.h"
18 #include "ui/gfx/image/image.h"
19 #include "ui/native_theme/common_theme.h"
20 #include "ui/resources/grit/ui_resources.h"
21 #include "ui/views/layout/layout_constants.h"
23 using namespace password_manager::mac::ui;
27 const CGFloat kBorderWidth = 1;
28 const SkColor kHoverColor = SkColorSetARGBInline(0xFF, 0xEB, 0xEB, 0xEB);
30 NSColor* HoverColor() {
31 return gfx::SkColorToCalibratedNSColor(kHoverColor);
35 return [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
38 NSSize LabelSize(int resourceID) {
39 return [l10n_util::GetNSString(resourceID)
40 sizeWithAttributes:@{NSFontAttributeName : LabelFont()}];
43 CGFloat FirstFieldWidth() {
44 const CGFloat undoExplanationWidth =
45 LabelSize(IDS_MANAGE_PASSWORDS_DELETED).width;
46 const CGFloat kUsernameWidth =
47 ManagePasswordsBubbleModel::UsernameFieldWidth();
48 const CGFloat width = std::max(kUsernameWidth, undoExplanationWidth);
52 CGFloat SecondFieldWidth() {
53 const CGFloat undoLinkWidth =
54 LabelSize(IDS_MANAGE_PASSWORDS_UNDO).width;
55 const CGFloat kPasswordWidth =
56 ManagePasswordsBubbleModel::PasswordFieldWidth();
57 const CGFloat width = std::max(kPasswordWidth, undoLinkWidth);
65 views::kItemLabelSpacing +
67 views::kItemLabelSpacing +
68 chrome_style::GetCloseButtonSize() +
73 void InitLabel(NSTextField* textField, const base::string16& text) {
74 [textField setStringValue:base::SysUTF16ToNSString(text)];
75 [textField setEditable:NO];
76 [textField setSelectable:NO];
77 [textField setDrawsBackground:NO];
78 [textField setBezeled:NO];
79 [textField setFont:LabelFont()];
80 [textField sizeToFit];
83 NSTextField* Label(const base::string16& text) {
84 base::scoped_nsobject<NSTextField> textField(
85 [[NSTextField alloc] initWithFrame:NSZeroRect]);
86 InitLabel(textField, text);
87 return textField.autorelease();
90 NSTextField* UsernameLabel(const base::string16& text) {
91 NSTextField* textField = Label(text);
93 setFrameSize:NSMakeSize(FirstFieldWidth(), NSHeight([textField frame]))];
97 NSSecureTextField* PasswordLabel(const base::string16& text) {
98 base::scoped_nsobject<NSSecureTextField> textField(
99 [[NSSecureTextField alloc] initWithFrame:NSZeroRect]);
100 InitLabel(textField, text);
102 setFrameSize:NSMakeSize(SecondFieldWidth(), NSHeight([textField frame]))];
103 return textField.autorelease();
108 @implementation ManagePasswordItemUndoView
109 - (id)initWithTarget:(id)target action:(SEL)action {
110 if ((self = [super init])) {
111 // The button should look like a link.
112 undoButton_.reset([[NSButton alloc] initWithFrame:NSZeroRect]);
113 base::scoped_nsobject<HyperlinkButtonCell> cell([[HyperlinkButtonCell alloc]
114 initTextCell:l10n_util::GetNSString(IDS_MANAGE_PASSWORDS_UNDO)]);
115 [cell setControlSize:NSSmallControlSize];
116 [cell setShouldUnderline:NO];
117 [cell setUnderlineOnHover:NO];
118 [cell setTextColor:gfx::SkColorToCalibratedNSColor(
119 chrome_style::GetLinkColor())];
120 [undoButton_ setCell:cell.get()];
121 [undoButton_ sizeToFit];
122 [undoButton_ setTarget:target];
123 [undoButton_ setAction:action];
125 const CGFloat width = ItemWidth();
126 CGFloat curX = kFramePadding;
127 CGFloat curY = views::kRelatedControlVerticalSpacing;
129 // Add the explanation text.
131 Label(l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_DELETED));
132 [label setFrameOrigin:NSMakePoint(curX, curY)];
133 [self addSubview:label];
135 // The undo button should be right-aligned.
136 curX = width - kFramePadding - NSWidth([undoButton_ frame]);
137 [undoButton_ setFrameOrigin:NSMakePoint(curX, curY)];
138 [self addSubview:undoButton_ ];
140 // Move to the top-right of the delete button.
141 curX = NSMaxX([undoButton_ frame]) + kFramePadding;
142 curY = NSMaxY([undoButton_ frame]) + views::kRelatedControlVerticalSpacing;
145 DCHECK_EQ(width, curX);
146 [self setFrameSize:NSMakeSize(curX, curY)];
152 @implementation ManagePasswordItemUndoView (Testing)
153 - (NSButton*)undoButton {
154 return undoButton_.get();
158 @implementation ManagePasswordItemManageView
159 - (id)initWithForm:(const autofill::PasswordForm&)form
162 if ((self = [super init])) {
163 deleteButton_.reset([[HoverImageButton alloc] initWithFrame:NSZeroRect]);
164 [deleteButton_ setFrameSize:NSMakeSize(chrome_style::GetCloseButtonSize(),
165 chrome_style::GetCloseButtonSize())];
166 [deleteButton_ setBordered:NO];
167 [[deleteButton_ cell] setHighlightsBy:NSNoCellMask];
168 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
170 setDefaultImage:bundle.GetImageNamed(IDR_CLOSE_2).ToNSImage()];
172 setHoverImage:bundle.GetImageNamed(IDR_CLOSE_2_H).ToNSImage()];
174 setPressedImage:bundle.GetImageNamed(IDR_CLOSE_2_P).ToNSImage()];
175 [deleteButton_ setTarget:target];
176 [deleteButton_ setAction:action];
178 const CGFloat width = ItemWidth();
179 CGFloat curX = kFramePadding;
180 CGFloat curY = views::kRelatedControlVerticalSpacing;
183 usernameField_.reset([UsernameLabel(form.username_value) retain]);
184 [usernameField_ setFrameOrigin:NSMakePoint(curX, curY)];
185 [self addSubview:usernameField_];
187 // Move to the right of the username and add the password.
188 curX = NSMaxX([usernameField_ frame]) + views::kItemLabelSpacing;
189 passwordField_.reset([PasswordLabel(form.password_value) retain]);
190 [passwordField_ setFrameOrigin:NSMakePoint(curX, curY)];
191 [self addSubview:passwordField_];
193 // The delete button should be right-aligned.
194 curX = width - kFramePadding - NSWidth([deleteButton_ frame]);
195 [deleteButton_ setFrameOrigin:NSMakePoint(curX, curY)];
196 [self addSubview:deleteButton_];
198 // Move to the top-right of the delete button.
199 curX = NSMaxX([deleteButton_ frame]) + kFramePadding;
201 NSMaxY([deleteButton_ frame]) + views::kRelatedControlVerticalSpacing;
204 DCHECK_EQ(width, curX);
205 [self setFrameSize:NSMakeSize(curX, curY)];
211 @implementation ManagePasswordItemManageView (Testing)
212 - (NSTextField*)usernameField {
213 return usernameField_.get();
215 - (NSSecureTextField*)passwordField {
216 return passwordField_.get();
218 - (NSButton*)deleteButton {
219 return deleteButton_.get();
223 @implementation ManagePasswordItemPendingView
225 - (id)initWithForm:(const autofill::PasswordForm&)form {
226 if ((self = [super initWithFrame:NSZeroRect])) {
227 CGFloat curX = kFramePadding;
228 CGFloat curY = views::kRelatedControlVerticalSpacing;
231 usernameField_.reset([UsernameLabel(form.username_value) retain]);
232 [usernameField_ setFrameOrigin:NSMakePoint(curX, curY)];
233 [self addSubview:usernameField_];
235 // Move to the right of the username and add the password.
236 curX = NSMaxX([usernameField_ frame]) + views::kItemLabelSpacing;
237 passwordField_.reset([PasswordLabel(form.password_value) retain]);
238 [passwordField_ setFrameOrigin:NSMakePoint(curX, curY)];
239 [self addSubview:passwordField_];
241 // Move to the top-right of the password.
243 NSMaxY([passwordField_ frame]) + views::kRelatedControlVerticalSpacing;
246 [self setFrameSize:NSMakeSize(ItemWidth(), curY)];
253 @implementation ManagePasswordItemPendingView (Testing)
255 - (NSTextField*)usernameField {
256 return usernameField_.get();
259 - (NSSecureTextField*)passwordField {
260 return passwordField_.get();
265 @interface ManagePasswordItemViewController ()
266 - (void)onDeleteClicked:(id)sender;
267 - (void)onUndoClicked:(id)sender;
269 // Find the next content view and repaint.
272 // Find the next content view.
273 - (void)updateContent;
275 // Repaint the content.
276 - (void)layoutContent;
279 @implementation ManagePasswordItemViewController
281 - (id)initWithModel:(ManagePasswordsBubbleModel*)model
282 passwordForm:(const autofill::PasswordForm&)passwordForm
283 position:(password_manager::ui::PasswordItemPosition)position {
284 if ((self = [super initWithNibName:nil bundle:nil])) {
286 position_ = position;
287 passwordForm_ = passwordForm;
288 state_ = password_manager::ui::IsPendingState(model_->state())
289 ? MANAGE_PASSWORD_ITEM_STATE_PENDING
290 : MANAGE_PASSWORD_ITEM_STATE_MANAGE;
291 [self updateContent];
296 - (void)onDeleteClicked:(id)sender {
297 DCHECK_EQ(MANAGE_PASSWORD_ITEM_STATE_MANAGE, state_);
298 state_ = MANAGE_PASSWORD_ITEM_STATE_DELETED;
300 model_->OnPasswordAction(passwordForm_,
301 ManagePasswordsBubbleModel::REMOVE_PASSWORD);
304 - (void)onUndoClicked:(id)sender {
305 DCHECK_EQ(MANAGE_PASSWORD_ITEM_STATE_DELETED, state_);
306 state_ = MANAGE_PASSWORD_ITEM_STATE_MANAGE;
308 model_->OnPasswordAction(passwordForm_,
309 ManagePasswordsBubbleModel::ADD_PASSWORD);
313 [self updateContent];
314 [self layoutContent];
317 - (void)updateContent {
321 case MANAGE_PASSWORD_ITEM_STATE_PENDING:
323 [[ManagePasswordItemPendingView alloc] initWithForm:passwordForm_]);
325 case MANAGE_PASSWORD_ITEM_STATE_MANAGE:
326 contentView_.reset([[ManagePasswordItemManageView alloc]
327 initWithForm:passwordForm_
329 action:@selector(onDeleteClicked:)]);
331 case MANAGE_PASSWORD_ITEM_STATE_DELETED:
332 contentView_.reset([[ManagePasswordItemUndoView alloc]
334 action:@selector(onUndoClicked:)]);
339 - (void)layoutContent {
340 // Update the view size according to the content view size.
341 const NSSize contentSize = [contentView_ frame].size;
342 [self.view setFrameSize:contentSize];
345 [self.view setSubviews:@[ contentView_ ]];
347 // Add the borders, which go along the entire view.
348 SkColor borderSkColor;
349 ui::CommonThemeGetSystemColor(
350 ui::NativeTheme::kColorId_EnabledMenuButtonBorderColor, &borderSkColor);
351 CGColorRef borderColor = gfx::CGColorCreateFromSkColor(borderSkColor);
353 // Mac views don't have backing layers by default.
354 base::scoped_nsobject<CALayer> rootLayer([[CALayer alloc] init]);
355 [rootLayer setFrame:NSRectToCGRect(self.view.frame)];
356 [self.view setLayer:rootLayer];
357 [self.view setWantsLayer:YES];
359 // The top border is only present for the first item.
360 if (position_ == password_manager::ui::FIRST_ITEM) {
361 base::scoped_nsobject<CALayer> topBorder([[CALayer alloc] init]);
362 [topBorder setBackgroundColor:borderColor];
363 [topBorder setFrame:CGRectMake(0,
364 contentSize.height - kBorderWidth,
367 [self.view.layer addSublayer:topBorder];
370 // The bottom border is always present.
371 base::scoped_nsobject<CALayer> bottomBorder([[CALayer alloc] init]);
372 [bottomBorder setBackgroundColor:borderColor];
373 [bottomBorder setFrame:CGRectMake(0, 0, contentSize.width, kBorderWidth)];
374 [self.view.layer addSublayer:bottomBorder];
378 self.view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease];
379 [self layoutContent];
384 @implementation ManagePasswordItemViewController (Testing)
386 - (ManagePasswordItemState)state {
390 - (NSView*)contentView {
391 return contentView_.get();
394 - (autofill::PasswordForm)passwordForm {
395 return passwordForm_;
400 @implementation ManagePasswordItemClickableView
402 - (void)drawRect:(NSRect)dirtyRect {
403 [super drawRect:dirtyRect];
405 [HoverColor() setFill];
406 NSRectFill(dirtyRect);
410 - (void)mouseEntered:(NSEvent*)event {
412 [self setNeedsDisplay:YES];
415 - (void)mouseExited:(NSEvent*)event {
417 [self setNeedsDisplay:YES];
420 - (void)updateTrackingAreas {
421 [super updateTrackingAreas];
422 if (trackingArea_.get())
423 [self removeTrackingArea:trackingArea_.get()];
424 NSTrackingAreaOptions options =
425 NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow;
426 trackingArea_.reset([[CrTrackingArea alloc] initWithRect:[self bounds]
430 [self addTrackingArea:trackingArea_.get()];