1 // Copyright 2013 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/autofill_notification_controller.h"
9 #include "base/logging.h"
10 #include "base/mac/foundation_util.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "chrome/browser/ui/autofill/autofill_dialog_types.h"
14 #include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h"
15 #include "chrome/browser/ui/chrome_style.h"
16 #include "chrome/browser/ui/cocoa/autofill/autofill_dialog_constants.h"
17 #import "chrome/browser/ui/cocoa/autofill/autofill_tooltip_controller.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"
22 @interface AutofillNotificationView : NSView {
24 // Weak, determines anchor point for arrow.
25 NSView* arrowAnchorView_;
27 base::scoped_nsobject<NSColor> backgroundColor_;
28 base::scoped_nsobject<NSColor> borderColor_;
31 @property (nonatomic, assign) NSView* anchorView;
32 @property (nonatomic, assign) BOOL hasArrow;
33 @property (nonatomic, retain) NSColor* backgroundColor;
34 @property (nonatomic, retain) NSColor* borderColor;
38 @implementation AutofillNotificationView
40 @synthesize hasArrow = hasArrow_;
41 @synthesize anchorView = arrowAnchorView_;
43 - (void)drawRect:(NSRect)dirtyRect {
44 [super drawRect:dirtyRect];
47 NSRect bounds = [self bounds];
49 path = [NSBezierPath bezierPathWithRect:bounds];
51 // The upper tip of the arrow.
52 NSPoint anchorPoint = NSMakePoint(NSMidX([arrowAnchorView_ bounds]), 0);
53 anchorPoint = [self convertPoint:anchorPoint fromView:arrowAnchorView_];
54 anchorPoint.y = NSMaxY(bounds);
55 // The minimal rectangle that encloses the arrow.
56 NSRect arrowRect = NSMakeRect(anchorPoint.x - autofill::kArrowWidth / 2.0,
57 anchorPoint.y - autofill::kArrowHeight,
58 autofill::kArrowWidth,
59 autofill::kArrowHeight);
61 // Include the arrow and the rectangular non-arrow region in the same path,
62 // so that the stroke is easier to draw. Start at the upper-left of the
63 // rectangular region, and proceed clockwise.
64 path = [NSBezierPath bezierPath];
65 [path moveToPoint:NSMakePoint(NSMinX(bounds), NSMinY(arrowRect))];
66 [path lineToPoint:arrowRect.origin];
67 [path lineToPoint:NSMakePoint(NSMidX(arrowRect), NSMaxY(arrowRect))];
68 [path lineToPoint:NSMakePoint(NSMaxX(arrowRect), NSMinY(arrowRect))];
69 [path lineToPoint:NSMakePoint(NSMaxX(bounds), NSMinY(arrowRect))];
70 [path lineToPoint:NSMakePoint(NSMaxX(bounds), NSMinY(bounds))];
71 [path lineToPoint:NSMakePoint(NSMinX(bounds), NSMinY(bounds))];
75 [backgroundColor_ setFill];
77 [borderColor_ setStroke];
81 - (NSColor*)backgroundColor {
82 return backgroundColor_;
85 - (void)setBackgroundColor:(NSColor*)backgroundColor {
86 backgroundColor_.reset([backgroundColor retain]);
89 - (NSColor*)borderColor {
93 - (void)setBorderColor:(NSColor*)borderColor {
94 borderColor_.reset([borderColor retain]);
99 @implementation AutofillNotificationController
101 - (id)initWithNotification:(const autofill::DialogNotification*)notification
102 delegate:(autofill::AutofillDialogViewDelegate*)delegate {
103 if (self = [super init]) {
104 delegate_ = delegate;
105 notificationType_ = notification->type();
107 base::scoped_nsobject<AutofillNotificationView> view(
108 [[AutofillNotificationView alloc] initWithFrame:NSZeroRect]);
109 [view setBackgroundColor:
110 gfx::SkColorToCalibratedNSColor(notification->GetBackgroundColor())];
111 [view setBorderColor:
112 gfx::SkColorToCalibratedNSColor(notification->GetBorderColor())];
115 textview_.reset([[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
117 gfx::SkColorToCalibratedNSColor(notification->GetTextColor());
118 [textview_ setMessage:base::SysUTF16ToNSString(notification->display_text())
119 withFont:[NSFont labelFontOfSize:[[textview_ font] pointSize]]
120 messageColor:textColor];
121 if (!notification->link_range().is_empty()) {
122 // This class is not currently able to render links as checkbox labels.
123 DCHECK(!notification->HasCheckbox());
124 [textview_ setDelegate:self];
125 [textview_ addLinkRange:notification->link_range().ToNSRange()
127 linkColor:[NSColor blueColor]];
128 linkURL_ = notification->link_url();
130 [textview_ setHidden:notification->HasCheckbox()];
132 checkbox_.reset([[NSButton alloc] initWithFrame:NSZeroRect]);
133 [checkbox_ setButtonType:NSSwitchButton];
134 [checkbox_ setHidden:!notification->HasCheckbox()];
135 [checkbox_ setState:(notification->checked() ? NSOnState : NSOffState)];
136 [checkbox_ setAttributedTitle:[textview_ textStorage]];
137 [checkbox_ setTarget:self];
138 [checkbox_ setAction:@selector(checkboxClicked:)];
139 // Set the size that preferredSizeForWidth will use. Do this here because
140 // (1) preferredSizeForWidth is logically const, and so shouldn't have a
141 // side-effect of updating the checkbox's frame, and
142 // (2) this way, the sizing computation can be cached.
143 [checkbox_ sizeToFit];
145 tooltipController_.reset([[AutofillTooltipController alloc]
146 initWithArrowLocation:info_bubble::kTopRight]);
147 [tooltipController_ setImage:
148 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
149 IDR_AUTOFILL_TOOLTIP_ICON).ToNSImage()];
150 [tooltipController_ setMessage:
151 base::SysUTF16ToNSString(notification->tooltip_text())];
152 [[tooltipController_ view] setHidden:
153 [[tooltipController_ message] length] == 0];
155 [view setSubviews:@[ textview_, checkbox_, [tooltipController_ view] ]];
160 - (AutofillNotificationView*)notificationView {
161 return base::mac::ObjCCastStrict<AutofillNotificationView>([self view]);
164 - (void)setHasArrow:(BOOL)hasArrow withAnchorView:(NSView*)anchorView {
165 [[self notificationView] setAnchorView:anchorView];
166 [[self notificationView] setHasArrow:hasArrow];
170 return [[self notificationView] hasArrow];
173 - (NSTextView*)textview {
177 - (NSButton*)checkbox {
181 - (NSView*)tooltipView {
182 return [tooltipController_ view];
185 - (NSSize)preferredSizeForWidth:(CGFloat)width {
186 width -= 2 * chrome_style::kHorizontalPadding;
187 if (![[tooltipController_ view] isHidden]) {
188 width -= NSWidth([[tooltipController_ view] frame]) +
189 chrome_style::kHorizontalPadding;
191 // TODO(isherman): Restore the DCHECK below once I figure out why it causes
192 // unit tests to fail.
193 //DCHECK_GT(width, 0);
195 NSSize preferredSize;
196 if (![textview_ isHidden]) {
197 // This method is logically const. Hence, cache the original frame so that
198 // it can be restored once the preferred size has been computed.
199 NSRect frame = [textview_ frame];
201 // Compute preferred size.
202 [textview_ setFrameSize:NSMakeSize(width, frame.size.height)];
203 [textview_ setVerticallyResizable:YES];
204 [textview_ sizeToFit];
205 preferredSize = [textview_ frame].size;
207 // Restore original properties, since this method is logically const.
208 [textview_ setFrame:frame];
209 [textview_ setVerticallyResizable:NO];
211 // Unlike textfields, checkboxes (NSButtons, really) are not designed to
212 // support multi-line labels. Hence, ignore the |width| and simply use the
213 // size that fits fit the checkbox's contents.
214 // NOTE: This logic will need to be updated if there is ever a need to
215 // support checkboxes with multi-line labels.
216 DCHECK(![checkbox_ isHidden]);
217 preferredSize = [checkbox_ frame].size;
220 if ([[self notificationView] hasArrow])
221 preferredSize.height += autofill::kArrowHeight;
223 preferredSize.height += 2 * autofill::kNotificationPadding;
224 return preferredSize;
227 - (NSSize)preferredSize {
232 - (void)performLayout {
233 NSRect bounds = [[self view] bounds];
234 if ([[self notificationView] hasArrow])
235 bounds.size.height -= autofill::kArrowHeight;
237 // Calculate the frame size, leaving room for padding around the notification,
238 // as well as for the tooltip if it is visible.
239 NSRect labelFrame = NSInsetRect(bounds,
240 chrome_style::kHorizontalPadding,
241 autofill::kNotificationPadding);
242 NSView* tooltipView = [tooltipController_ view];
243 if (![tooltipView isHidden]) {
244 labelFrame.size.width -=
245 NSWidth([tooltipView frame]) + chrome_style::kHorizontalPadding;
248 NSView* label = [checkbox_ isHidden] ? textview_.get() : checkbox_.get();
249 [label setFrame:labelFrame];
251 if (![tooltipView isHidden]) {
252 NSPoint tooltipOrigin =
254 NSMaxX(labelFrame) + chrome_style::kHorizontalPadding,
255 NSMidY(labelFrame) - (NSHeight([tooltipView frame]) / 2.0));
256 [tooltipView setFrameOrigin:tooltipOrigin];
260 - (IBAction)checkboxClicked:(id)sender {
261 DCHECK(sender == checkbox_.get());
262 BOOL isChecked = ([checkbox_ state] == NSOnState);
263 delegate_->NotificationCheckboxStateChanged(notificationType_, isChecked);
266 - (BOOL)textView:(NSTextView *)textView
267 clickedOnLink:(id)link
268 atIndex:(NSUInteger)charIndex {
269 delegate_->LinkClicked(linkURL_);