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. 4 5 #import "chrome/browser/ui/cocoa/base_bubble_controller.h" 6 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/tabs/tab_strip_model_observer_bridge.h" 16 #include "grit/generated_resources.h" 17 #include "ui/base/l10n/l10n_util.h" 18 19 @interface BaseBubbleController (Private) 20 - (void)registerForNotifications; 21 - (void)updateOriginFromAnchor; 22 - (void)activateTabWithContents:(content::WebContents*)newContents 23 previousContents:(content::WebContents*)oldContents 24 atIndex:(NSInteger)index 25 reason:(int)reason; 26 - (void)recordAnchorOffset; 27 - (void)parentWindowDidResize:(NSNotification*)notification; 28 - (void)parentWindowWillClose:(NSNotification*)notification; 29 - (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification; 30 - (void)closeCleanup; 31 @end 32 33 @implementation BaseBubbleController 34 35 @synthesize parentWindow = parentWindow_; 36 @synthesize anchorPoint = anchor_; 37 @synthesize bubble = bubble_; 38 @synthesize shouldOpenAsKeyWindow = shouldOpenAsKeyWindow_; 39 @synthesize shouldCloseOnResignKey = shouldCloseOnResignKey_; 40 41 - (id)initWithWindowNibPath:(NSString*)nibPath 42 parentWindow:(NSWindow*)parentWindow 43 anchoredAt:(NSPoint)anchoredAt { 44 nibPath = [base::mac::FrameworkBundle() pathForResource:nibPath 45 ofType:@"nib"]; 46 if ((self = [super initWithWindowNibPath:nibPath owner:self])) { 47 parentWindow_ = parentWindow; 48 anchor_ = anchoredAt; 49 shouldOpenAsKeyWindow_ = YES; 50 shouldCloseOnResignKey_ = YES; 51 [self registerForNotifications]; 52 } 53 return self; 54 } 55 56 - (id)initWithWindowNibPath:(NSString*)nibPath 57 relativeToView:(NSView*)view 58 offset:(NSPoint)offset { 59 DCHECK([view window]); 60 NSWindow* window = [view window]; 61 NSRect bounds = [view convertRect:[view bounds] toView:nil]; 62 NSPoint anchor = NSMakePoint(NSMinX(bounds) + offset.x, 63 NSMinY(bounds) + offset.y); 64 anchor = [window convertBaseToScreen:anchor]; 65 return [self initWithWindowNibPath:nibPath 66 parentWindow:window 67 anchoredAt:anchor]; 68 } 69 70 - (id)initWithWindow:(NSWindow*)theWindow 71 parentWindow:(NSWindow*)parentWindow 72 anchoredAt:(NSPoint)anchoredAt { 73 DCHECK(theWindow); 74 if ((self = [super initWithWindow:theWindow])) { 75 parentWindow_ = parentWindow; 76 anchor_ = anchoredAt; 77 shouldOpenAsKeyWindow_ = YES; 78 shouldCloseOnResignKey_ = YES; 79 80 DCHECK(![[self window] delegate]); 81 [theWindow setDelegate:self]; 82 83 base::scoped_nsobject<InfoBubbleView> contentView( 84 [[InfoBubbleView alloc] initWithFrame:NSZeroRect]); 85 [theWindow setContentView:contentView.get()]; 86 bubble_ = contentView.get(); 87 88 [self registerForNotifications]; 89 [self awakeFromNib]; 90 } 91 return self; 92 } 93 94 - (void)awakeFromNib { 95 // Check all connections have been made in Interface Builder. 96 DCHECK([self window]); 97 DCHECK(bubble_); 98 DCHECK_EQ(self, [[self window] delegate]); 99 100 BrowserWindowController* bwc = 101 [BrowserWindowController browserWindowControllerForWindow:parentWindow_]; 102 if (bwc) { 103 TabStripController* tabStripController = [bwc tabStripController]; 104 TabStripModel* tabStripModel = [tabStripController tabStripModel]; 105 tabStripObserverBridge_.reset(new TabStripModelObserverBridge(tabStripModel, 106 self)); 107 } 108 109 [bubble_ setArrowLocation:info_bubble::kTopRight]; 110 } 111 112 - (void)dealloc { 113 [[NSNotificationCenter defaultCenter] removeObserver:self]; 114 [super dealloc]; 115 } 116 117 - (void)registerForNotifications { 118 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 119 // Watch to see if the parent window closes, and if so, close this one. 120 [center addObserver:self 121 selector:@selector(parentWindowWillClose:) 122 name:NSWindowWillCloseNotification 123 object:parentWindow_]; 124 // Watch for the full screen event, if so, close the bubble 125 [center addObserver:self 126 selector:@selector(parentWindowWillBecomeFullScreen:) 127 name:NSWindowWillEnterFullScreenNotification 128 object:parentWindow_]; 129 // Watch for parent window's resizing, to ensure this one is always 130 // anchored correctly. 131 [center addObserver:self 132 selector:@selector(parentWindowDidResize:) 133 name:NSWindowDidResizeNotification 134 object:parentWindow_]; 135 } 136 137 - (void)setAnchorPoint:(NSPoint)anchor { 138 anchor_ = anchor; 139 [self updateOriginFromAnchor]; 140 } 141 142 - (void)recordAnchorOffset { 143 // The offset of the anchor from the parent's upper-left-hand corner is kept 144 // to ensure the bubble stays anchored correctly if the parent is resized. 145 anchorOffset_ = NSMakePoint(NSMinX([parentWindow_ frame]), 146 NSMaxY([parentWindow_ frame])); 147 anchorOffset_.x -= anchor_.x; 148 anchorOffset_.y -= anchor_.y; 149 } 150 151 - (NSBox*)horizontalSeparatorWithFrame:(NSRect)frame { 152 frame.size.height = 1.0; 153 base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]); 154 [spacer setBoxType:NSBoxSeparator]; 155 [spacer setBorderType:NSLineBorder]; 156 [spacer setAlphaValue:0.2]; 157 return [spacer.release() autorelease]; 158 } 159 160 - (NSBox*)verticalSeparatorWithFrame:(NSRect)frame { 161 frame.size.width = 1.0; 162 base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]); 163 [spacer setBoxType:NSBoxSeparator]; 164 [spacer setBorderType:NSLineBorder]; 165 [spacer setAlphaValue:0.2]; 166 return [spacer.release() autorelease]; 167 } 168 169 - (void)parentWindowDidResize:(NSNotification*)notification { 170 if (!parentWindow_) 171 return; 172 173 DCHECK_EQ(parentWindow_, [notification object]); 174 NSPoint newOrigin = NSMakePoint(NSMinX([parentWindow_ frame]), 175 NSMaxY([parentWindow_ frame])); 176 newOrigin.x -= anchorOffset_.x; 177 newOrigin.y -= anchorOffset_.y; 178 [self setAnchorPoint:newOrigin]; 179 } 180 181 - (void)parentWindowWillClose:(NSNotification*)notification { 182 parentWindow_ = nil; 183 [self close]; 184 } 185 186 - (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification { 187 parentWindow_ = nil; 188 [self close]; 189 } 190 191 - (void)closeCleanup { 192 if (eventTap_) { 193 [NSEvent removeMonitor:eventTap_]; 194 eventTap_ = nil; 195 } 196 if (resignationObserver_) { 197 [[NSNotificationCenter defaultCenter] 198 removeObserver:resignationObserver_ 199 name:NSWindowDidResignKeyNotification 200 object:nil]; 201 resignationObserver_ = nil; 202 } 203 204 tabStripObserverBridge_.reset(); 205 206 NSWindow* window = [self window]; 207 [[window parentWindow] removeChildWindow:window]; 208 } 209 210 - (void)windowWillClose:(NSNotification*)notification { 211 [self closeCleanup]; 212 [[NSNotificationCenter defaultCenter] removeObserver:self]; 213 [self autorelease]; 214 } 215 216 // We want this to be a child of a browser window. addChildWindow: 217 // (called from this function) will bring the window on-screen; 218 // unfortunately, [NSWindowController showWindow:] will also bring it 219 // on-screen (but will cause unexpected changes to the window's 220 // position). We cannot have an addChildWindow: and a subsequent 221 // showWindow:. Thus, we have our own version. 222 - (void)showWindow:(id)sender { 223 NSWindow* window = [self window]; // Completes nib load. 224 [self updateOriginFromAnchor]; 225 [parentWindow_ addChildWindow:window ordered:NSWindowAbove]; 226 if (shouldOpenAsKeyWindow_) 227 [window makeKeyAndOrderFront:self]; 228 else 229 [window orderFront:nil]; 230 [self registerKeyStateEventTap]; 231 [self recordAnchorOffset]; 232 } 233 234 - (void)close { 235 [self closeCleanup]; 236 [super close]; 237 } 238 239 // The controller is the delegate of the window so it receives did resign key 240 // notifications. When key is resigned mirror Windows behavior and close the 241 // window. 242 - (void)windowDidResignKey:(NSNotification*)notification { 243 NSWindow* window = [self window]; 244 DCHECK_EQ([notification object], window); 245 if ([window isVisible] && [self shouldCloseOnResignKey]) { 246 // If the window isn't visible, it is already closed, and this notification 247 // has been sent as part of the closing operation, so no need to close. 248 [self close]; 249 } 250 } 251 252 // Since the bubble shares first responder with its parent window, set 253 // event handlers to dismiss the bubble when it would normally lose key 254 // state. 255 - (void)registerKeyStateEventTap { 256 // Parent key state sharing is only avaiable on 10.7+. 257 if (!base::mac::IsOSLionOrLater()) 258 return; 259 260 NSWindow* window = self.window; 261 NSNotification* note = 262 [NSNotification notificationWithName:NSWindowDidResignKeyNotification 263 object:window]; 264 265 // The eventTap_ catches clicks within the application that are outside the 266 // window. 267 eventTap_ = [NSEvent 268 addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask | 269 NSRightMouseDownMask 270 handler:^NSEvent* (NSEvent* event) { 271 if (event.window != window) { 272 // Do it right now, because if this event is right mouse event, 273 // it may pop up a menu. windowDidResignKey: will not run until 274 // the menu is closed. 275 if ([self respondsToSelector:@selector(windowDidResignKey:)]) { 276 [self windowDidResignKey:note]; 277 } 278 } 279 return event; 280 }]; 281 282 // The resignationObserver_ watches for when a window resigns key state, 283 // meaning the key window has changed and the bubble should be dismissed. 284 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 285 resignationObserver_ = 286 [center addObserverForName:NSWindowDidResignKeyNotification 287 object:nil 288 queue:[NSOperationQueue mainQueue] 289 usingBlock:^(NSNotification* notif) { 290 [self windowDidResignKey:note]; 291 }]; 292 } 293 294 // By implementing this, ESC causes the window to go away. 295 - (IBAction)cancel:(id)sender { 296 // This is not a "real" cancel as potential changes to the radio group are not 297 // undone. That's ok. 298 [self close]; 299 } 300 301 // Takes the |anchor_| point and adjusts the window's origin accordingly. 302 - (void)updateOriginFromAnchor { 303 NSWindow* window = [self window]; 304 NSPoint origin = anchor_; 305 306 switch ([bubble_ alignment]) { 307 case info_bubble::kAlignArrowToAnchor: { 308 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset + 309 info_bubble::kBubbleArrowWidth / 2.0, 0); 310 offsets = [[parentWindow_ contentView] convertSize:offsets toView:nil]; 311 switch ([bubble_ arrowLocation]) { 312 case info_bubble::kTopRight: 313 origin.x -= NSWidth([window frame]) - offsets.width; 314 break; 315 case info_bubble::kTopLeft: 316 origin.x -= offsets.width; 317 break; 318 case info_bubble::kTopCenter: 319 origin.x -= NSWidth([window frame]) / 2.0; 320 break; 321 case info_bubble::kNoArrow: 322 NOTREACHED(); 323 break; 324 } 325 break; 326 } 327 328 case info_bubble::kAlignEdgeToAnchorEdge: 329 // If the arrow is to the right then move the origin so that the right 330 // edge aligns with the anchor. If the arrow is to the left then there's 331 // nothing to do because the left edge is already aligned with the left 332 // edge of the anchor. 333 if ([bubble_ arrowLocation] == info_bubble::kTopRight) { 334 origin.x -= NSWidth([window frame]); 335 } 336 break; 337 338 case info_bubble::kAlignRightEdgeToAnchorEdge: 339 origin.x -= NSWidth([window frame]); 340 break; 341 342 case info_bubble::kAlignLeftEdgeToAnchorEdge: 343 // Nothing to do. 344 break; 345 346 default: 347 NOTREACHED(); 348 } 349 350 origin.y -= NSHeight([window frame]); 351 [window setFrameOrigin:origin]; 352 } 353 354 - (void)activateTabWithContents:(content::WebContents*)newContents 355 previousContents:(content::WebContents*)oldContents 356 atIndex:(NSInteger)index 357 reason:(int)reason { 358 // The user switched tabs; close. 359 [self close]; 360 } 361 362 @end // BaseBubbleController 363