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/mac/sdk_forward_declarations.h"
12 #include "base/strings/string_util.h"
13 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
14 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
15 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
16 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.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;
28 - (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification;
32 @implementation BaseBubbleController
34 @synthesize parentWindow = parentWindow_;
35 @synthesize anchorPoint = anchor_;
36 @synthesize bubble = bubble_;
37 @synthesize shouldOpenAsKeyWindow = shouldOpenAsKeyWindow_;
38 @synthesize shouldCloseOnResignKey = shouldCloseOnResignKey_;
40 - (id)initWithWindowNibPath:(NSString*)nibPath
41 parentWindow:(NSWindow*)parentWindow
42 anchoredAt:(NSPoint)anchoredAt {
43 nibPath = [base::mac::FrameworkBundle() pathForResource:nibPath
45 if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
46 parentWindow_ = parentWindow;
48 shouldOpenAsKeyWindow_ = YES;
49 shouldCloseOnResignKey_ = YES;
50 [self registerForNotifications];
55 - (id)initWithWindowNibPath:(NSString*)nibPath
56 relativeToView:(NSView*)view
57 offset:(NSPoint)offset {
58 DCHECK([view window]);
59 NSWindow* window = [view window];
60 NSRect bounds = [view convertRect:[view bounds] toView:nil];
61 NSPoint anchor = NSMakePoint(NSMinX(bounds) + offset.x,
62 NSMinY(bounds) + offset.y);
63 anchor = [window convertBaseToScreen:anchor];
64 return [self initWithWindowNibPath:nibPath
69 - (id)initWithWindow:(NSWindow*)theWindow
70 parentWindow:(NSWindow*)parentWindow
71 anchoredAt:(NSPoint)anchoredAt {
73 if ((self = [super initWithWindow:theWindow])) {
74 parentWindow_ = parentWindow;
76 shouldOpenAsKeyWindow_ = YES;
77 shouldCloseOnResignKey_ = YES;
79 DCHECK(![[self window] delegate]);
80 [theWindow setDelegate:self];
82 base::scoped_nsobject<InfoBubbleView> contentView(
83 [[InfoBubbleView alloc] initWithFrame:NSZeroRect]);
84 [theWindow setContentView:contentView.get()];
85 bubble_ = contentView.get();
87 [self registerForNotifications];
93 - (void)awakeFromNib {
94 // Check all connections have been made in Interface Builder.
95 DCHECK([self window]);
97 DCHECK_EQ(self, [[self window] delegate]);
99 BrowserWindowController* bwc =
100 [BrowserWindowController browserWindowControllerForWindow:parentWindow_];
102 TabStripController* tabStripController = [bwc tabStripController];
103 TabStripModel* tabStripModel = [tabStripController tabStripModel];
104 tabStripObserverBridge_.reset(new TabStripModelObserverBridge(tabStripModel,
108 [bubble_ setArrowLocation:info_bubble::kTopRight];
112 [[NSNotificationCenter defaultCenter] removeObserver:self];
116 - (void)registerForNotifications {
117 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
118 // Watch to see if the parent window closes, and if so, close this one.
119 [center addObserver:self
120 selector:@selector(parentWindowWillClose:)
121 name:NSWindowWillCloseNotification
122 object:parentWindow_];
123 // Watch for the full screen event, if so, close the bubble
124 [center addObserver:self
125 selector:@selector(parentWindowWillBecomeFullScreen:)
126 name:NSWindowWillEnterFullScreenNotification
127 object:parentWindow_];
128 // Watch for parent window's resizing, to ensure this one is always
129 // anchored correctly.
130 [center addObserver:self
131 selector:@selector(parentWindowDidResize:)
132 name:NSWindowDidResizeNotification
133 object:parentWindow_];
136 - (void)setAnchorPoint:(NSPoint)anchor {
138 [self updateOriginFromAnchor];
141 - (void)recordAnchorOffset {
142 // The offset of the anchor from the parent's upper-left-hand corner is kept
143 // to ensure the bubble stays anchored correctly if the parent is resized.
144 anchorOffset_ = NSMakePoint(NSMinX([parentWindow_ frame]),
145 NSMaxY([parentWindow_ frame]));
146 anchorOffset_.x -= anchor_.x;
147 anchorOffset_.y -= anchor_.y;
150 - (NSBox*)horizontalSeparatorWithFrame:(NSRect)frame {
151 frame.size.height = 1.0;
152 base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
153 [spacer setBoxType:NSBoxSeparator];
154 [spacer setBorderType:NSLineBorder];
155 [spacer setAlphaValue:0.2];
156 return [spacer.release() autorelease];
159 - (NSBox*)verticalSeparatorWithFrame:(NSRect)frame {
160 frame.size.width = 1.0;
161 base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
162 [spacer setBoxType:NSBoxSeparator];
163 [spacer setBorderType:NSLineBorder];
164 [spacer setAlphaValue:0.2];
165 return [spacer.release() autorelease];
168 - (void)parentWindowDidResize:(NSNotification*)notification {
172 DCHECK_EQ(parentWindow_, [notification object]);
173 NSPoint newOrigin = NSMakePoint(NSMinX([parentWindow_ frame]),
174 NSMaxY([parentWindow_ frame]));
175 newOrigin.x -= anchorOffset_.x;
176 newOrigin.y -= anchorOffset_.y;
177 [self setAnchorPoint:newOrigin];
180 - (void)parentWindowWillClose:(NSNotification*)notification {
185 - (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification {
190 - (void)closeCleanup {
192 [NSEvent removeMonitor:eventTap_];
195 if (resignationObserver_) {
196 [[NSNotificationCenter defaultCenter]
197 removeObserver:resignationObserver_
198 name:NSWindowDidResignKeyNotification
200 resignationObserver_ = nil;
203 tabStripObserverBridge_.reset();
205 NSWindow* window = [self window];
206 [[window parentWindow] removeChildWindow:window];
209 - (void)windowWillClose:(NSNotification*)notification {
211 [[NSNotificationCenter defaultCenter] removeObserver:self];
215 // We want this to be a child of a browser window. addChildWindow:
216 // (called from this function) will bring the window on-screen;
217 // unfortunately, [NSWindowController showWindow:] will also bring it
218 // on-screen (but will cause unexpected changes to the window's
219 // position). We cannot have an addChildWindow: and a subsequent
220 // showWindow:. Thus, we have our own version.
221 - (void)showWindow:(id)sender {
222 NSWindow* window = [self window]; // Completes nib load.
223 [self updateOriginFromAnchor];
224 [parentWindow_ addChildWindow:window ordered:NSWindowAbove];
225 if (shouldOpenAsKeyWindow_)
226 [window makeKeyAndOrderFront:self];
228 [window orderFront:nil];
229 [self registerKeyStateEventTap];
230 [self recordAnchorOffset];
238 // The controller is the delegate of the window so it receives did resign key
239 // notifications. When key is resigned mirror Windows behavior and close the
241 - (void)windowDidResignKey:(NSNotification*)notification {
242 NSWindow* window = [self window];
243 DCHECK_EQ([notification object], window);
244 if ([window isVisible] && [self shouldCloseOnResignKey]) {
245 // If the window isn't visible, it is already closed, and this notification
246 // has been sent as part of the closing operation, so no need to close.
248 } else if ([window isVisible]) {
249 // The bubble should not receive key events when it is no longer key window,
250 // so disable sharing parent key state. Share parent key state is only used
251 // to enable the close/minimize/maximize buttons of the parent window when
252 // the bubble has key state, so disabling it here is safe.
253 InfoBubbleWindow* bubbleWindow =
254 base::mac::ObjCCastStrict<InfoBubbleWindow>([self window]);
255 [bubbleWindow setAllowShareParentKeyState:NO];
259 - (void)windowDidBecomeKey:(NSNotification*)notification {
260 // Re-enable share parent key state to make sure the close/minimize/maximize
261 // buttons of the parent window are active.
262 InfoBubbleWindow* bubbleWindow =
263 base::mac::ObjCCastStrict<InfoBubbleWindow>([self window]);
264 [bubbleWindow setAllowShareParentKeyState:YES];
267 // Since the bubble shares first responder with its parent window, set
268 // event handlers to dismiss the bubble when it would normally lose key
270 - (void)registerKeyStateEventTap {
271 // Parent key state sharing is only avaiable on 10.7+.
272 if (!base::mac::IsOSLionOrLater())
275 NSWindow* window = self.window;
276 NSNotification* note =
277 [NSNotification notificationWithName:NSWindowDidResignKeyNotification
280 // The eventTap_ catches clicks within the application that are outside the
283 addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask |
285 handler:^NSEvent* (NSEvent* event) {
286 if (event.window != window) {
287 // Do it right now, because if this event is right mouse event,
288 // it may pop up a menu. windowDidResignKey: will not run until
289 // the menu is closed.
290 if ([self respondsToSelector:@selector(windowDidResignKey:)]) {
291 [self windowDidResignKey:note];
297 // The resignationObserver_ watches for when a window resigns key state,
298 // meaning the key window has changed and the bubble should be dismissed.
299 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
300 resignationObserver_ =
301 [center addObserverForName:NSWindowDidResignKeyNotification
303 queue:[NSOperationQueue mainQueue]
304 usingBlock:^(NSNotification* notif) {
305 [self windowDidResignKey:note];
309 // By implementing this, ESC causes the window to go away.
310 - (IBAction)cancel:(id)sender {
311 // This is not a "real" cancel as potential changes to the radio group are not
312 // undone. That's ok.
316 // Takes the |anchor_| point and adjusts the window's origin accordingly.
317 - (void)updateOriginFromAnchor {
318 NSWindow* window = [self window];
319 NSPoint origin = anchor_;
321 switch ([bubble_ alignment]) {
322 case info_bubble::kAlignArrowToAnchor: {
323 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
324 info_bubble::kBubbleArrowWidth / 2.0, 0);
325 offsets = [[parentWindow_ contentView] convertSize:offsets toView:nil];
326 switch ([bubble_ arrowLocation]) {
327 case info_bubble::kTopRight:
328 origin.x -= NSWidth([window frame]) - offsets.width;
330 case info_bubble::kTopLeft:
331 origin.x -= offsets.width;
333 case info_bubble::kTopCenter:
334 origin.x -= NSWidth([window frame]) / 2.0;
336 case info_bubble::kNoArrow:
343 case info_bubble::kAlignEdgeToAnchorEdge:
344 // If the arrow is to the right then move the origin so that the right
345 // edge aligns with the anchor. If the arrow is to the left then there's
346 // nothing to do because the left edge is already aligned with the left
347 // edge of the anchor.
348 if ([bubble_ arrowLocation] == info_bubble::kTopRight) {
349 origin.x -= NSWidth([window frame]);
353 case info_bubble::kAlignRightEdgeToAnchorEdge:
354 origin.x -= NSWidth([window frame]);
357 case info_bubble::kAlignLeftEdgeToAnchorEdge:
365 origin.y -= NSHeight([window frame]);
366 [window setFrameOrigin:origin];
369 - (void)activateTabWithContents:(content::WebContents*)newContents
370 previousContents:(content::WebContents*)oldContents
371 atIndex:(NSInteger)index
373 // The user switched tabs; close.
377 @end // BaseBubbleController