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