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