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. 4 5#import "chrome/browser/ui/cocoa/autofill/autofill_notification_controller.h" 6 7#include <algorithm> 8 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" 21 22@interface AutofillNotificationView : NSView { 23 @private 24 // Weak, determines anchor point for arrow. 25 NSView* arrowAnchorView_; 26 BOOL hasArrow_; 27 base::scoped_nsobject<NSColor> backgroundColor_; 28 base::scoped_nsobject<NSColor> borderColor_; 29} 30 31@property (nonatomic, assign) NSView* anchorView; 32@property (nonatomic, assign) BOOL hasArrow; 33@property (nonatomic, retain) NSColor* backgroundColor; 34@property (nonatomic, retain) NSColor* borderColor; 35 36@end 37 38@implementation AutofillNotificationView 39 40@synthesize hasArrow = hasArrow_; 41@synthesize anchorView = arrowAnchorView_; 42 43- (void)drawRect:(NSRect)dirtyRect { 44 [super drawRect:dirtyRect]; 45 46 NSBezierPath* path; 47 NSRect bounds = [self bounds]; 48 if (!hasArrow_) { 49 path = [NSBezierPath bezierPathWithRect:bounds]; 50 } else { 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); 60 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))]; 72 [path closePath]; 73 } 74 75 [backgroundColor_ setFill]; 76 [path fill]; 77 [borderColor_ setStroke]; 78 [path stroke]; 79} 80 81- (NSColor*)backgroundColor { 82 return backgroundColor_; 83} 84 85- (void)setBackgroundColor:(NSColor*)backgroundColor { 86 backgroundColor_.reset([backgroundColor retain]); 87} 88 89- (NSColor*)borderColor { 90 return borderColor_; 91} 92 93- (void)setBorderColor:(NSColor*)borderColor { 94 borderColor_.reset([borderColor retain]); 95} 96 97@end 98 99@implementation AutofillNotificationController 100 101- (id)initWithNotification:(const autofill::DialogNotification*)notification 102 delegate:(autofill::AutofillDialogViewDelegate*)delegate { 103 if (self = [super init]) { 104 delegate_ = delegate; 105 notificationType_ = notification->type(); 106 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())]; 113 [self setView:view]; 114 115 textview_.reset([[HyperlinkTextView alloc] initWithFrame:NSZeroRect]); 116 NSColor* textColor = 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() 126 withName:self 127 linkColor:[NSColor blueColor]]; 128 linkURL_ = notification->link_url(); 129 } 130 [textview_ setHidden:notification->HasCheckbox()]; 131 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]; 144 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]; 154 155 [view setSubviews:@[ textview_, checkbox_, [tooltipController_ view] ]]; 156 } 157 return self; 158} 159 160- (AutofillNotificationView*)notificationView { 161 return base::mac::ObjCCastStrict<AutofillNotificationView>([self view]); 162} 163 164- (void)setHasArrow:(BOOL)hasArrow withAnchorView:(NSView*)anchorView { 165 [[self notificationView] setAnchorView:anchorView]; 166 [[self notificationView] setHasArrow:hasArrow]; 167} 168 169- (BOOL)hasArrow { 170 return [[self notificationView] hasArrow]; 171} 172 173- (NSTextView*)textview { 174 return textview_; 175} 176 177- (NSButton*)checkbox { 178 return checkbox_; 179} 180 181- (NSView*)tooltipView { 182 return [tooltipController_ view]; 183} 184 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; 190 } 191 // TODO(isherman): Restore the DCHECK below once I figure out why it causes 192 // unit tests to fail. 193 //DCHECK_GT(width, 0); 194 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]; 200 201 // Compute preferred size. 202 [textview_ setFrameSize:NSMakeSize(width, frame.size.height)]; 203 [textview_ setVerticallyResizable:YES]; 204 [textview_ sizeToFit]; 205 preferredSize = [textview_ frame].size; 206 207 // Restore original properties, since this method is logically const. 208 [textview_ setFrame:frame]; 209 [textview_ setVerticallyResizable:NO]; 210 } else { 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; 218 } 219 220 if ([[self notificationView] hasArrow]) 221 preferredSize.height += autofill::kArrowHeight; 222 223 preferredSize.height += 2 * autofill::kNotificationPadding; 224 return preferredSize; 225} 226 227- (NSSize)preferredSize { 228 NOTREACHED(); 229 return NSZeroSize; 230} 231 232- (void)performLayout { 233 NSRect bounds = [[self view] bounds]; 234 if ([[self notificationView] hasArrow]) 235 bounds.size.height -= autofill::kArrowHeight; 236 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; 246 } 247 248 NSView* label = [checkbox_ isHidden] ? textview_.get() : checkbox_.get(); 249 [label setFrame:labelFrame]; 250 251 if (![tooltipView isHidden]) { 252 NSPoint tooltipOrigin = 253 NSMakePoint( 254 NSMaxX(labelFrame) + chrome_style::kHorizontalPadding, 255 NSMidY(labelFrame) - (NSHeight([tooltipView frame]) / 2.0)); 256 [tooltipView setFrameOrigin:tooltipOrigin]; 257 } 258} 259 260- (IBAction)checkboxClicked:(id)sender { 261 DCHECK(sender == checkbox_.get()); 262 BOOL isChecked = ([checkbox_ state] == NSOnState); 263 delegate_->NotificationCheckboxStateChanged(notificationType_, isChecked); 264} 265 266- (BOOL)textView:(NSTextView *)textView 267 clickedOnLink:(id)link 268 atIndex:(NSUInteger)charIndex { 269 delegate_->LinkClicked(linkURL_); 270 return YES; 271} 272 273@end 274