1 // Copyright (c) 2012 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/base_bubble_controller.h"
7 #include "base/logging.h"
8 #include "base/mac/bundle_locations.h"
9 #include "base/mac/mac_util.h"
10 #include "base/mac/scoped_nsobject.h"
11 #include "base/strings/string_util.h"
12 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
13 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
14 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
15 #include "grit/generated_resources.h"
16 #include "ui/base/l10n/l10n_util.h"
18 @interface BaseBubbleController (Private)
19 - (void)registerForNotifications;
20 - (void)updateOriginFromAnchor;
21 - (void)activateTabWithContents:(content::WebContents*)newContents
22 previousContents:(content::WebContents*)oldContents
23 atIndex:(NSInteger)index
25 - (void)recordAnchorOffset;
26 - (void)parentWindowDidResize:(NSNotification*)notification;
27 - (void)parentWindowWillClose:(NSNotification*)notification;
31 @implementation BaseBubbleController
33 @synthesize parentWindow = parentWindow_;
34 @synthesize anchorPoint = anchor_;
35 @synthesize bubble = bubble_;
36 @synthesize shouldOpenAsKeyWindow = shouldOpenAsKeyWindow_;
37 @synthesize shouldCloseOnResignKey = shouldCloseOnResignKey_;
39 - (id)initWithWindowNibPath:(NSString*)nibPath
40 parentWindow:(NSWindow*)parentWindow
41 anchoredAt:(NSPoint)anchoredAt {
42 nibPath = [base::mac::FrameworkBundle() pathForResource:nibPath
44 if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
45 parentWindow_ = parentWindow;
47 shouldOpenAsKeyWindow_ = YES;
48 shouldCloseOnResignKey_ = YES;
49 [self registerForNotifications];
54 - (id)initWithWindowNibPath:(NSString*)nibPath
55 relativeToView:(NSView*)view
56 offset:(NSPoint)offset {
57 DCHECK([view window]);
58 NSWindow* window = [view window];
59 NSRect bounds = [view convertRect:[view bounds] toView:nil];
60 NSPoint anchor = NSMakePoint(NSMinX(bounds) + offset.x,
61 NSMinY(bounds) + offset.y);
62 anchor = [window convertBaseToScreen:anchor];
63 return [self initWithWindowNibPath:nibPath
68 - (id)initWithWindow:(NSWindow*)theWindow
69 parentWindow:(NSWindow*)parentWindow
70 anchoredAt:(NSPoint)anchoredAt {
72 if ((self = [super initWithWindow:theWindow])) {
73 parentWindow_ = parentWindow;
75 shouldOpenAsKeyWindow_ = YES;
76 shouldCloseOnResignKey_ = YES;
78 DCHECK(![[self window] delegate]);
79 [theWindow setDelegate:self];
81 base::scoped_nsobject<InfoBubbleView> contentView(
82 [[InfoBubbleView alloc] initWithFrame:NSZeroRect]);
83 [theWindow setContentView:contentView.get()];
84 bubble_ = contentView.get();
86 [self registerForNotifications];
92 - (void)awakeFromNib {
93 // Check all connections have been made in Interface Builder.
94 DCHECK([self window]);
96 DCHECK_EQ(self, [[self window] delegate]);
98 BrowserWindowController* bwc =
99 [BrowserWindowController browserWindowControllerForWindow:parentWindow_];
101 TabStripController* tabStripController = [bwc tabStripController];
102 TabStripModel* tabStripModel = [tabStripController tabStripModel];
103 tabStripObserverBridge_.reset(new TabStripModelObserverBridge(tabStripModel,
107 [bubble_ setArrowLocation:info_bubble::kTopRight];
111 [[NSNotificationCenter defaultCenter] removeObserver:self];
115 - (void)registerForNotifications {
116 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
117 // Watch to see if the parent window closes, and if so, close this one.
118 [center addObserver:self
119 selector:@selector(parentWindowWillClose:)
120 name:NSWindowWillCloseNotification
121 object:parentWindow_];
122 // Watch for parent window's resizing, to ensure this one is always
123 // anchored correctly.
124 [center addObserver:self
125 selector:@selector(parentWindowDidResize:)
126 name:NSWindowDidResizeNotification
127 object:parentWindow_];
130 - (void)setAnchorPoint:(NSPoint)anchor {
132 [self updateOriginFromAnchor];
135 - (void)recordAnchorOffset {
136 // The offset of the anchor from the parent's upper-left-hand corner is kept
137 // to ensure the bubble stays anchored correctly if the parent is resized.
138 anchorOffset_ = NSMakePoint(NSMinX([parentWindow_ frame]),
139 NSMaxY([parentWindow_ frame]));
140 anchorOffset_.x -= anchor_.x;
141 anchorOffset_.y -= anchor_.y;
144 - (NSBox*)separatorWithFrame:(NSRect)frame {
145 frame.size.height = 1.0;
146 base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
147 [spacer setBoxType:NSBoxSeparator];
148 [spacer setBorderType:NSLineBorder];
149 [spacer setAlphaValue:0.2];
150 return [spacer.release() autorelease];
153 - (void)parentWindowDidResize:(NSNotification*)notification {
154 DCHECK_EQ(parentWindow_, [notification object]);
155 NSPoint newOrigin = NSMakePoint(NSMinX([parentWindow_ frame]),
156 NSMaxY([parentWindow_ frame]));
157 newOrigin.x -= anchorOffset_.x;
158 newOrigin.y -= anchorOffset_.y;
159 [self setAnchorPoint:newOrigin];
162 - (void)parentWindowWillClose:(NSNotification*)notification {
167 - (void)closeCleanup {
169 [NSEvent removeMonitor:eventTap_];
172 if (resignationObserver_) {
173 [[NSNotificationCenter defaultCenter]
174 removeObserver:resignationObserver_
175 name:NSWindowDidResignKeyNotification
177 resignationObserver_ = nil;
180 tabStripObserverBridge_.reset();
182 NSWindow* window = [self window];
183 [[window parentWindow] removeChildWindow:window];
186 - (void)windowWillClose:(NSNotification*)notification {
188 [[NSNotificationCenter defaultCenter] removeObserver:self];
192 // We want this to be a child of a browser window. addChildWindow:
193 // (called from this function) will bring the window on-screen;
194 // unfortunately, [NSWindowController showWindow:] will also bring it
195 // on-screen (but will cause unexpected changes to the window's
196 // position). We cannot have an addChildWindow: and a subsequent
197 // showWindow:. Thus, we have our own version.
198 - (void)showWindow:(id)sender {
199 NSWindow* window = [self window]; // Completes nib load.
200 [self updateOriginFromAnchor];
201 [parentWindow_ addChildWindow:window ordered:NSWindowAbove];
202 if (shouldOpenAsKeyWindow_)
203 [window makeKeyAndOrderFront:self];
205 [window orderFront:nil];
206 [self registerKeyStateEventTap];
207 [self recordAnchorOffset];
215 // The controller is the delegate of the window so it receives did resign key
216 // notifications. When key is resigned mirror Windows behavior and close the
218 - (void)windowDidResignKey:(NSNotification*)notification {
219 NSWindow* window = [self window];
220 DCHECK_EQ([notification object], window);
221 if ([window isVisible] && [self shouldCloseOnResignKey]) {
222 // If the window isn't visible, it is already closed, and this notification
223 // has been sent as part of the closing operation, so no need to close.
228 // Since the bubble shares first responder with its parent window, set
229 // event handlers to dismiss the bubble when it would normally lose key
231 - (void)registerKeyStateEventTap {
232 // Parent key state sharing is only avaiable on 10.7+.
233 if (!base::mac::IsOSLionOrLater())
236 NSWindow* window = self.window;
237 NSNotification* note =
238 [NSNotification notificationWithName:NSWindowDidResignKeyNotification
241 // The eventTap_ catches clicks within the application that are outside the
244 addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask
245 handler:^NSEvent* (NSEvent* event) {
246 if (event.window != window) {
247 // Call via the runloop because this block is called in the
248 // middle of event dispatch.
249 [self performSelector:@selector(windowDidResignKey:)
256 // The resignationObserver_ watches for when a window resigns key state,
257 // meaning the key window has changed and the bubble should be dismissed.
258 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
259 resignationObserver_ =
260 [center addObserverForName:NSWindowDidResignKeyNotification
262 queue:[NSOperationQueue mainQueue]
263 usingBlock:^(NSNotification* notif) {
264 [self windowDidResignKey:note];
268 // By implementing this, ESC causes the window to go away.
269 - (IBAction)cancel:(id)sender {
270 // This is not a "real" cancel as potential changes to the radio group are not
271 // undone. That's ok.
275 // Takes the |anchor_| point and adjusts the window's origin accordingly.
276 - (void)updateOriginFromAnchor {
277 NSWindow* window = [self window];
278 NSPoint origin = anchor_;
280 switch ([bubble_ alignment]) {
281 case info_bubble::kAlignArrowToAnchor: {
282 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
283 info_bubble::kBubbleArrowWidth / 2.0, 0);
284 offsets = [[parentWindow_ contentView] convertSize:offsets toView:nil];
285 switch ([bubble_ arrowLocation]) {
286 case info_bubble::kTopRight:
287 origin.x -= NSWidth([window frame]) - offsets.width;
289 case info_bubble::kTopLeft:
290 origin.x -= offsets.width;
292 case info_bubble::kTopCenter:
293 origin.x -= NSWidth([window frame]) / 2.0;
295 case info_bubble::kNoArrow:
302 case info_bubble::kAlignEdgeToAnchorEdge:
303 // If the arrow is to the right then move the origin so that the right
304 // edge aligns with the anchor. If the arrow is to the left then there's
305 // nothing to do because the left edge is already aligned with the left
306 // edge of the anchor.
307 if ([bubble_ arrowLocation] == info_bubble::kTopRight) {
308 origin.x -= NSWidth([window frame]);
312 case info_bubble::kAlignRightEdgeToAnchorEdge:
313 origin.x -= NSWidth([window frame]);
316 case info_bubble::kAlignLeftEdgeToAnchorEdge:
324 origin.y -= NSHeight([window frame]);
325 [window setFrameOrigin:origin];
328 - (void)activateTabWithContents:(content::WebContents*)newContents
329 previousContents:(content::WebContents*)oldContents
330 atIndex:(NSInteger)index
332 // The user switched tabs; close.
336 @end // BaseBubbleController