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