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/info_bubble_window.h" 16 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h" 17 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 24 reason:(int)reason; 25 - (void)recordAnchorOffset; 26 - (void)parentWindowDidResize:(NSNotification*)notification; 27 - (void)parentWindowWillClose:(NSNotification*)notification; 28 - (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification; 29 - (void)closeCleanup; 30 @end 31 32 @implementation BaseBubbleController 33 34 @synthesize parentWindow = parentWindow_; 35 @synthesize anchorPoint = anchor_; 36 @synthesize bubble = bubble_; 37 @synthesize shouldOpenAsKeyWindow = shouldOpenAsKeyWindow_; 38 @synthesize shouldCloseOnResignKey = shouldCloseOnResignKey_; 39 40 - (id)initWithWindowNibPath:(NSString*)nibPath 41 parentWindow:(NSWindow*)parentWindow 42 anchoredAt:(NSPoint)anchoredAt { 43 nibPath = [base::mac::FrameworkBundle() pathForResource:nibPath 44 ofType:@"nib"]; 45 if ((self = [super initWithWindowNibPath:nibPath owner:self])) { 46 parentWindow_ = parentWindow; 47 anchor_ = anchoredAt; 48 shouldOpenAsKeyWindow_ = YES; 49 shouldCloseOnResignKey_ = YES; 50 [self registerForNotifications]; 51 } 52 return self; 53 } 54 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 65 parentWindow:window 66 anchoredAt:anchor]; 67 } 68 69 - (id)initWithWindow:(NSWindow*)theWindow 70 parentWindow:(NSWindow*)parentWindow 71 anchoredAt:(NSPoint)anchoredAt { 72 DCHECK(theWindow); 73 if ((self = [super initWithWindow:theWindow])) { 74 parentWindow_ = parentWindow; 75 shouldOpenAsKeyWindow_ = YES; 76 shouldCloseOnResignKey_ = YES; 77 78 DCHECK(![[self window] delegate]); 79 [theWindow setDelegate:self]; 80 81 base::scoped_nsobject<InfoBubbleView> contentView( 82 [[InfoBubbleView alloc] initWithFrame:NSZeroRect]); 83 [theWindow setContentView:contentView.get()]; 84 bubble_ = contentView.get(); 85 86 [self registerForNotifications]; 87 [self awakeFromNib]; 88 [self setAnchorPoint:anchoredAt]; 89 } 90 return self; 91 } 92 93 - (void)awakeFromNib { 94 // Check all connections have been made in Interface Builder. 95 DCHECK([self window]); 96 DCHECK(bubble_); 97 DCHECK_EQ(self, [[self window] delegate]); 98 99 BrowserWindowController* bwc = 100 [BrowserWindowController browserWindowControllerForWindow:parentWindow_]; 101 if (bwc) { 102 TabStripController* tabStripController = [bwc tabStripController]; 103 TabStripModel* tabStripModel = [tabStripController tabStripModel]; 104 tabStripObserverBridge_.reset(new TabStripModelObserverBridge(tabStripModel, 105 self)); 106 } 107 108 [bubble_ setArrowLocation:info_bubble::kTopRight]; 109 } 110 111 - (void)dealloc { 112 [[NSNotificationCenter defaultCenter] removeObserver:self]; 113 [super dealloc]; 114 } 115 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_]; 134 } 135 136 - (void)setAnchorPoint:(NSPoint)anchor { 137 anchor_ = anchor; 138 [self updateOriginFromAnchor]; 139 } 140 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; 148 } 149 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]; 157 } 158 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]; 166 } 167 168 - (void)parentWindowDidResize:(NSNotification*)notification { 169 if (!parentWindow_) 170 return; 171 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]; 178 } 179 180 - (void)parentWindowWillClose:(NSNotification*)notification { 181 parentWindow_ = nil; 182 [self close]; 183 } 184 185 - (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification { 186 parentWindow_ = nil; 187 [self close]; 188 } 189 190 - (void)closeCleanup { 191 if (eventTap_) { 192 [NSEvent removeMonitor:eventTap_]; 193 eventTap_ = nil; 194 } 195 if (resignationObserver_) { 196 [[NSNotificationCenter defaultCenter] 197 removeObserver:resignationObserver_ 198 name:NSWindowDidResignKeyNotification 199 object:nil]; 200 resignationObserver_ = nil; 201 } 202 203 tabStripObserverBridge_.reset(); 204 205 NSWindow* window = [self window]; 206 [[window parentWindow] removeChildWindow:window]; 207 } 208 209 - (void)windowWillClose:(NSNotification*)notification { 210 [self closeCleanup]; 211 [[NSNotificationCenter defaultCenter] removeObserver:self]; 212 [self autorelease]; 213 } 214 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]; 227 else 228 [window orderFront:nil]; 229 [self registerKeyStateEventTap]; 230 [self recordAnchorOffset]; 231 } 232 233 - (void)close { 234 [self closeCleanup]; 235 [super close]; 236 } 237 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 240 // window. 241 - (void)windowDidResignKey:(NSNotification*)notification { 242 NSWindow* window = [self window]; 243 DCHECK_EQ([notification object], window); 244 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. 247 if (![window isVisible]) 248 return; 249 250 // Don't close when explicily disabled, or if there's an attached sheet (e.g. 251 // Open File dialog). 252 if ([self shouldCloseOnResignKey] && ![window attachedSheet]) { 253 [self close]; 254 return; 255 } 256 257 // The bubble should not receive key events when it is no longer key window, 258 // so disable sharing parent key state. Share parent key state is only used 259 // to enable the close/minimize/maximize buttons of the parent window when 260 // the bubble has key state, so disabling it here is safe. 261 InfoBubbleWindow* bubbleWindow = 262 base::mac::ObjCCastStrict<InfoBubbleWindow>([self window]); 263 [bubbleWindow setAllowShareParentKeyState:NO]; 264 } 265 266 - (void)windowDidBecomeKey:(NSNotification*)notification { 267 // Re-enable share parent key state to make sure the close/minimize/maximize 268 // buttons of the parent window are active. 269 InfoBubbleWindow* bubbleWindow = 270 base::mac::ObjCCastStrict<InfoBubbleWindow>([self window]); 271 [bubbleWindow setAllowShareParentKeyState:YES]; 272 } 273 274 // Since the bubble shares first responder with its parent window, set event 275 // handlers to dismiss the bubble when it would normally lose key state. 276 // Events on sheets are ignored: this assumes the sheet belongs to the bubble 277 // since, to affect a sheet on a different window, the bubble would also lose 278 // key status in -[NSWindowDelegate windowDidResignKey:]. This keeps the logic 279 // simple, since -[NSWindow attachedSheet] returns nil while the sheet is still 280 // closing. 281 - (void)registerKeyStateEventTap { 282 // Parent key state sharing is only avaiable on 10.7+. 283 if (!base::mac::IsOSLionOrLater()) 284 return; 285 286 NSWindow* window = self.window; 287 NSNotification* note = 288 [NSNotification notificationWithName:NSWindowDidResignKeyNotification 289 object:window]; 290 291 // The eventTap_ catches clicks within the application that are outside the 292 // window. 293 eventTap_ = [NSEvent 294 addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask | 295 NSRightMouseDownMask 296 handler:^NSEvent* (NSEvent* event) { 297 if ([event window] != window && ![[event window] isSheet]) { 298 // Do it right now, because if this event is right mouse event, 299 // it may pop up a menu. windowDidResignKey: will not run until 300 // the menu is closed. 301 if ([self respondsToSelector:@selector(windowDidResignKey:)]) { 302 [self windowDidResignKey:note]; 303 } 304 } 305 return event; 306 }]; 307 308 // The resignationObserver_ watches for when a window resigns key state, 309 // meaning the key window has changed and the bubble should be dismissed. 310 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 311 resignationObserver_ = 312 [center addObserverForName:NSWindowDidResignKeyNotification 313 object:nil 314 queue:[NSOperationQueue mainQueue] 315 usingBlock:^(NSNotification* notif) { 316 if (![[notif object] isSheet]) 317 [self windowDidResignKey:note]; 318 }]; 319 } 320 321 // By implementing this, ESC causes the window to go away. 322 - (IBAction)cancel:(id)sender { 323 // This is not a "real" cancel as potential changes to the radio group are not 324 // undone. That's ok. 325 [self close]; 326 } 327 328 // Takes the |anchor_| point and adjusts the window's origin accordingly. 329 - (void)updateOriginFromAnchor { 330 NSWindow* window = [self window]; 331 NSPoint origin = anchor_; 332 333 switch ([bubble_ alignment]) { 334 case info_bubble::kAlignArrowToAnchor: { 335 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset + 336 info_bubble::kBubbleArrowWidth / 2.0, 0); 337 offsets = [[parentWindow_ contentView] convertSize:offsets toView:nil]; 338 switch ([bubble_ arrowLocation]) { 339 case info_bubble::kTopRight: 340 origin.x -= NSWidth([window frame]) - offsets.width; 341 break; 342 case info_bubble::kTopLeft: 343 origin.x -= offsets.width; 344 break; 345 case info_bubble::kTopCenter: 346 origin.x -= NSWidth([window frame]) / 2.0; 347 break; 348 case info_bubble::kNoArrow: 349 NOTREACHED(); 350 break; 351 } 352 break; 353 } 354 355 case info_bubble::kAlignEdgeToAnchorEdge: 356 // If the arrow is to the right then move the origin so that the right 357 // edge aligns with the anchor. If the arrow is to the left then there's 358 // nothing to do because the left edge is already aligned with the left 359 // edge of the anchor. 360 if ([bubble_ arrowLocation] == info_bubble::kTopRight) { 361 origin.x -= NSWidth([window frame]); 362 } 363 break; 364 365 case info_bubble::kAlignRightEdgeToAnchorEdge: 366 origin.x -= NSWidth([window frame]); 367 break; 368 369 case info_bubble::kAlignLeftEdgeToAnchorEdge: 370 // Nothing to do. 371 break; 372 373 default: 374 NOTREACHED(); 375 } 376 377 origin.y -= NSHeight([window frame]); 378 [window setFrameOrigin:origin]; 379 } 380 381 - (void)activateTabWithContents:(content::WebContents*)newContents 382 previousContents:(content::WebContents*)oldContents 383 atIndex:(NSInteger)index 384 reason:(int)reason { 385 // The user switched tabs; close. 386 [self close]; 387 } 388 389 @end // BaseBubbleController 390