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