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_tooltip_controller.h"
6
7#include "base/mac/foundation_util.h"
8#import "chrome/browser/ui/cocoa/autofill/autofill_bubble_controller.h"
9#import "ui/base/cocoa/base_view.h"
10#import "ui/base/cocoa/hover_image_button.h"
11
12// Delay time before tooltip shows/hides.
13const NSTimeInterval kTooltipDelay = 0.1;
14
15// How far to inset tooltip contents.
16CGFloat kTooltipInset = 10;
17
18#pragma mark AutofillTooltipController - private methods
19
20@interface AutofillTooltipController ()
21
22// Sets hover state for "mouse over InfoBubble".
23- (void)setHoveringOnBubble:(BOOL)hoveringOnBubble;
24
25// Update the combined hover state - if either button or bubble is hovered,
26// the combined state is considered "hovered". Notifies delegate if the state
27// changed.
28- (void)updateTooltipDisplayState;
29
30@end
31
32#pragma mark AutofillTooltip
33
34// The actual tooltip control - based on HoverButton, which comes with free
35// hover handling.
36@interface AutofillTooltip : HoverButton {
37 @private
38  // Not owned - |tooltipController_| owns this object.
39  AutofillTooltipController* tooltipController_;
40}
41
42@property(assign, nonatomic) AutofillTooltipController* tooltipController;
43
44@end
45
46
47@implementation AutofillTooltip
48
49@synthesize tooltipController = tooltipController_;
50
51- (void)drawRect:(NSRect)rect {
52 [[self image] drawInRect:rect
53                 fromRect:NSZeroRect
54                operation:NSCompositeSourceOver
55                 fraction:1.0
56           respectFlipped:YES
57                    hints:nil];
58}
59
60- (void)setHoverState:(HoverState)state {
61  [super setHoverState:state];
62  [tooltipController_ updateTooltipDisplayState];
63}
64
65- (BOOL)acceptsFirstResponder {
66  return NO;
67}
68
69@end
70
71#pragma mark AutofillTrackingView
72
73// A very basic view that only tracks mouseEntered:/mouseExited: and forwards
74// them to |tooltipController_|.
75@interface AutofillTrackingView : BaseView {
76 @private
77  // Not owned - tooltip controller owns tracking view and tooltip.
78  AutofillTooltipController* tooltipController_;
79}
80
81@property(assign, nonatomic) AutofillTooltipController* tooltipController;
82
83@end
84
85@implementation AutofillTrackingView
86
87@synthesize tooltipController = tooltipController_;
88
89- (void)mouseEntered:(NSEvent*)theEvent {
90  [tooltipController_ setHoveringOnBubble:YES];
91}
92
93- (void)mouseExited:(NSEvent*)theEvent {
94  [tooltipController_ setHoveringOnBubble:NO];
95}
96
97@end
98
99#pragma mark AutofillTooltipController
100
101@implementation AutofillTooltipController
102
103@synthesize message = message_;
104
105- (id)initWithArrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation {
106  if ((self = [super init])) {
107    arrowLocation_ = arrowLocation;
108    view_.reset([[AutofillTooltip alloc] init]);
109    [self setView:view_];
110    [view_ setTooltipController:self];
111  }
112  return self;
113}
114
115- (void)dealloc {
116  [view_ setTooltipController:nil];
117  [NSObject cancelPreviousPerformRequestsWithTarget:self];
118  [[NSNotificationCenter defaultCenter]
119      removeObserver:self
120                name:NSWindowWillCloseNotification
121              object:[bubbleController_ window]];
122  [super dealloc];
123}
124
125- (void)setImage:(NSImage*)image {
126  [view_ setImage:image];
127  [view_ setFrameSize:[image size]];
128}
129
130- (void)tooltipWindowWillClose:(NSNotification*)notification {
131  bubbleController_ = nil;
132}
133
134- (void)displayHover {
135  [bubbleController_ close];
136  bubbleController_ =
137    [[AutofillBubbleController alloc]
138        initWithParentWindow:[[self view] window]
139                     message:[self message]
140                       inset:NSMakeSize(kTooltipInset, kTooltipInset)
141               arrowLocation:arrowLocation_];
142  [bubbleController_ setShouldCloseOnResignKey:NO];
143
144  // Handle bubble self-deleting.
145  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
146  [center addObserver:self
147             selector:@selector(tooltipWindowWillClose:)
148                 name:NSWindowWillCloseNotification
149               object:[bubbleController_ window]];
150
151  // Inject a tracking view so controller can track hover events for the bubble.
152  base::scoped_nsobject<NSView> oldContentView(
153      [[[bubbleController_ window] contentView] retain]);
154  base::scoped_nsobject<AutofillTrackingView> trackingView(
155      [[AutofillTrackingView alloc] initWithFrame:[oldContentView frame]]);
156  [trackingView setTooltipController:self];
157  [trackingView setAutoresizesSubviews:YES];
158  [oldContentView setFrame:[trackingView bounds]];
159  [oldContentView
160      setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
161  [[bubbleController_ window] setContentView:trackingView];
162  [trackingView setSubviews:@[ oldContentView ]];
163
164  // Compute anchor point (in window coords - views might be flipped).
165  NSRect viewRect = [view_ convertRect:[view_ bounds] toView:nil];
166  NSPoint anchorPoint = NSMakePoint(NSMidX(viewRect), NSMinY(viewRect));
167  [bubbleController_ setAnchorPoint:
168      [[[self view] window] convertBaseToScreen:anchorPoint]];
169  [bubbleController_ showWindow:self];
170}
171
172- (void)hideHover {
173  [bubbleController_ close];
174}
175
176- (void)setHoveringOnBubble:(BOOL)hoveringOnBubble {
177  isHoveringOnBubble_ = hoveringOnBubble;
178  [self updateTooltipDisplayState];
179}
180
181- (void)updateTooltipDisplayState {
182  BOOL newDisplayState =
183      ([view_ hoverState] != kHoverStateNone || isHoveringOnBubble_);
184
185  if (newDisplayState != shouldDisplayTooltip_) {
186    shouldDisplayTooltip_ = newDisplayState;
187
188    // Cancel any pending visibility changes.
189    [NSObject cancelPreviousPerformRequestsWithTarget:self];
190
191    // If the desired visibility disagrees with current visibility, start a
192    // timer to change visibility. (Uses '!!' to force bool values)
193    if (!!bubbleController_ ^ !!shouldDisplayTooltip_) {
194      SEL sel = shouldDisplayTooltip_ ? @selector(displayHover)
195                                      : @selector(hideHover);
196      [self performSelector:sel withObject:nil afterDelay:kTooltipDelay];
197    }
198  }
199}
200
201@end
202