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/constrained_window/constrained_window_sheet_controller.h" 6 7 #include <map> 8 9 #include "base/logging.h" 10 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet.h" 11 #include "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet_info.h" 12 13 namespace { 14 15 // Maps parent windows to sheet controllers. 16 NSMutableDictionary* g_sheetControllers; 17 18 // Get a value for the given window that can be used as a key in a dictionary. 19 NSValue* GetKeyForParentWindow(NSWindow* parent_window) { 20 return [NSValue valueWithNonretainedObject:parent_window]; 21 } 22 23 } // namespace 24 25 // An invisible overlay window placed on top of the sheet's parent view. 26 // This window blocks interaction with the underlying view. 27 @interface CWSheetOverlayWindow : NSWindow { 28 base::scoped_nsobject<ConstrainedWindowSheetController> controller_; 29 } 30 @end 31 32 @interface ConstrainedWindowSheetController () 33 - (id)initWithParentWindow:(NSWindow*)parentWindow; 34 - (ConstrainedWindowSheetInfo*)findSheetInfoForParentView:(NSView*)parentView; 35 - (ConstrainedWindowSheetInfo*) 36 findSheetInfoForSheet:(id<ConstrainedWindowSheet>)sheet; 37 - (void)onParentWindowWillClose:(NSNotification*)note; 38 - (void)onParentWindowSizeDidChange:(NSNotification*)note; 39 - (void)updateSheetPosition:(NSView*)parentView; 40 - (NSRect)overlayWindowFrameForParentView:(NSView*)parentView; 41 - (NSPoint)originForSheetSize:(NSSize)sheetSize 42 inContainerRect:(NSRect)containerRect; 43 - (void)onOverlayWindowMouseDown:(CWSheetOverlayWindow*)overlayWindow; 44 - (void)closeSheet:(ConstrainedWindowSheetInfo*)info 45 withAnimation:(BOOL)withAnimation; 46 @end 47 48 @implementation CWSheetOverlayWindow 49 50 - (id)initWithContentRect:(NSRect)rect 51 controller:(ConstrainedWindowSheetController*)controller { 52 if ((self = [super initWithContentRect:rect 53 styleMask:NSBorderlessWindowMask 54 backing:NSBackingStoreBuffered 55 defer:NO])) { 56 [self setOpaque:NO]; 57 [self setBackgroundColor:[NSColor clearColor]]; 58 [self setIgnoresMouseEvents:NO]; 59 [self setReleasedWhenClosed:NO]; 60 controller_.reset([controller retain]); 61 } 62 return self; 63 } 64 65 - (void)mouseDown:(NSEvent*)event { 66 [controller_ onOverlayWindowMouseDown:self]; 67 } 68 69 @end 70 71 @implementation ConstrainedWindowSheetController 72 73 + (ConstrainedWindowSheetController*) 74 controllerForParentWindow:(NSWindow*)parentWindow { 75 DCHECK(parentWindow); 76 ConstrainedWindowSheetController* controller = 77 [g_sheetControllers objectForKey:GetKeyForParentWindow(parentWindow)]; 78 if (controller) 79 return controller; 80 81 base::scoped_nsobject<ConstrainedWindowSheetController> new_controller( 82 [[ConstrainedWindowSheetController alloc] 83 initWithParentWindow:parentWindow]); 84 if (!g_sheetControllers) 85 g_sheetControllers = [[NSMutableDictionary alloc] init]; 86 [g_sheetControllers setObject:new_controller 87 forKey:GetKeyForParentWindow(parentWindow)]; 88 return new_controller; 89 } 90 91 + (ConstrainedWindowSheetController*) 92 controllerForSheet:(id<ConstrainedWindowSheet>)sheet { 93 for (ConstrainedWindowSheetController* controller in 94 [g_sheetControllers objectEnumerator]) { 95 if ([controller findSheetInfoForSheet:sheet]) 96 return controller; 97 } 98 return nil; 99 } 100 101 + (id<ConstrainedWindowSheet>)sheetForOverlayWindow:(NSWindow*)overlayWindow { 102 for (ConstrainedWindowSheetController* controller in 103 [g_sheetControllers objectEnumerator]) { 104 for (ConstrainedWindowSheetInfo* info in controller->sheets_.get()) { 105 if ([overlayWindow isEqual:[info overlayWindow]]) 106 return [info sheet]; 107 } 108 } 109 return nil; 110 } 111 112 - (id)initWithParentWindow:(NSWindow*)parentWindow { 113 if ((self = [super init])) { 114 parentWindow_.reset([parentWindow retain]); 115 sheets_.reset([[NSMutableArray alloc] init]); 116 117 [[NSNotificationCenter defaultCenter] 118 addObserver:self 119 selector:@selector(onParentWindowWillClose:) 120 name:NSWindowWillCloseNotification 121 object:parentWindow_]; 122 } 123 return self; 124 } 125 126 - (void)showSheet:(id<ConstrainedWindowSheet>)sheet 127 forParentView:(NSView*)parentView { 128 DCHECK(sheet); 129 DCHECK(parentView); 130 if (!activeView_.get()) 131 activeView_.reset([parentView retain]); 132 133 // Observe the parent window's size. 134 [[NSNotificationCenter defaultCenter] 135 addObserver:self 136 selector:@selector(onParentWindowSizeDidChange:) 137 name:NSWindowDidResizeNotification 138 object:parentWindow_]; 139 140 // Create an invisible overlay window. 141 NSRect rect = [self overlayWindowFrameForParentView:parentView]; 142 base::scoped_nsobject<NSWindow> overlayWindow( 143 [[CWSheetOverlayWindow alloc] initWithContentRect:rect controller:self]); 144 [parentWindow_ addChildWindow:overlayWindow 145 ordered:NSWindowAbove]; 146 147 // Add an entry for the sheet. 148 base::scoped_nsobject<ConstrainedWindowSheetInfo> info( 149 [[ConstrainedWindowSheetInfo alloc] initWithSheet:sheet 150 parentView:parentView 151 overlayWindow:overlayWindow]); 152 [sheets_ addObject:info]; 153 154 // Show or hide the sheet. 155 if ([activeView_ isEqual:parentView]) 156 [info showSheet]; 157 else 158 [info hideSheet]; 159 } 160 161 - (NSPoint)originForSheet:(id<ConstrainedWindowSheet>)sheet 162 withWindowSize:(NSSize)size { 163 ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet]; 164 DCHECK(info); 165 NSRect containerRect = 166 [self overlayWindowFrameForParentView:[info parentView]]; 167 return [self originForSheetSize:size inContainerRect:containerRect]; 168 } 169 170 - (void)closeSheet:(id<ConstrainedWindowSheet>)sheet { 171 ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet]; 172 DCHECK(info); 173 [self closeSheet:info withAnimation:YES]; 174 } 175 176 - (void)parentViewDidBecomeActive:(NSView*)parentView { 177 [[self findSheetInfoForParentView:activeView_] hideSheet]; 178 activeView_.reset([parentView retain]); 179 [self updateSheetPosition:parentView]; 180 [[self findSheetInfoForParentView:activeView_] showSheet]; 181 } 182 183 - (void)pulseSheet:(id<ConstrainedWindowSheet>)sheet { 184 ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet]; 185 DCHECK(info); 186 if ([activeView_ isEqual:[info parentView]]) 187 [[info sheet] pulseSheet]; 188 } 189 190 - (int)sheetCount { 191 return [sheets_ count]; 192 } 193 194 - (ConstrainedWindowSheetInfo*)findSheetInfoForParentView:(NSView*)parentView { 195 for (ConstrainedWindowSheetInfo* info in sheets_.get()) { 196 if ([parentView isEqual:[info parentView]]) 197 return info; 198 } 199 return NULL; 200 } 201 202 - (ConstrainedWindowSheetInfo*) 203 findSheetInfoForSheet:(id<ConstrainedWindowSheet>)sheet { 204 for (ConstrainedWindowSheetInfo* info in sheets_.get()) { 205 if ([sheet isEqual:[info sheet]]) 206 return info; 207 } 208 return NULL; 209 } 210 211 - (void)onParentWindowWillClose:(NSNotification*)note { 212 [[NSNotificationCenter defaultCenter] 213 removeObserver:self 214 name:NSWindowWillCloseNotification 215 object:parentWindow_]; 216 217 // Close all sheets. 218 NSArray* sheets = [NSArray arrayWithArray:sheets_]; 219 for (ConstrainedWindowSheetInfo* info in sheets) 220 [self closeSheet:info withAnimation:NO]; 221 222 // Delete this instance. 223 [g_sheetControllers removeObjectForKey:GetKeyForParentWindow(parentWindow_)]; 224 if (![g_sheetControllers count]) { 225 [g_sheetControllers release]; 226 g_sheetControllers = nil; 227 } 228 } 229 230 - (void)onParentWindowSizeDidChange:(NSNotification*)note { 231 [self updateSheetPosition:activeView_]; 232 } 233 234 - (void)updateSheetPosition:(NSView*)parentView { 235 ConstrainedWindowSheetInfo* info = 236 [self findSheetInfoForParentView:parentView]; 237 if (!info) 238 return; 239 240 NSRect rect = [self overlayWindowFrameForParentView:parentView]; 241 [[info overlayWindow] setFrame:rect display:YES]; 242 [[info sheet] updateSheetPosition]; 243 } 244 245 - (NSRect)overlayWindowFrameForParentView:(NSView*)parentView { 246 NSRect viewFrame = [parentView convertRect:[parentView bounds] toView:nil]; 247 248 id<NSWindowDelegate> delegate = [[parentView window] delegate]; 249 if ([delegate respondsToSelector:@selector(window: 250 willPositionSheet: 251 usingRect:)]) { 252 NSRect sheetFrame = NSZeroRect; 253 // This API needs Y to be the distance from the bottom of the overlay to 254 // the top of the sheet. X, width, and height are ignored. 255 sheetFrame.origin.y = NSMaxY(viewFrame); 256 NSRect customSheetFrame = [delegate window:[parentView window] 257 willPositionSheet:nil 258 usingRect:sheetFrame]; 259 viewFrame.size.height += NSMinY(customSheetFrame) - NSMinY(sheetFrame); 260 } 261 262 viewFrame.origin = [[parentView window] convertBaseToScreen:viewFrame.origin]; 263 return viewFrame; 264 } 265 266 - (NSPoint)originForSheetSize:(NSSize)sheetSize 267 inContainerRect:(NSRect)containerRect { 268 NSPoint origin; 269 origin.x = roundf(NSMinX(containerRect) + 270 (NSWidth(containerRect) - sheetSize.width) / 2.0); 271 origin.y = NSMaxY(containerRect) + 5 - sheetSize.height; 272 return origin; 273 } 274 275 - (void)onOverlayWindowMouseDown:(CWSheetOverlayWindow*)overlayWindow { 276 for (ConstrainedWindowSheetInfo* curInfo in sheets_.get()) { 277 if ([overlayWindow isEqual:[curInfo overlayWindow]]) { 278 [self pulseSheet:[curInfo sheet]]; 279 [[curInfo sheet] makeSheetKeyAndOrderFront]; 280 break; 281 } 282 } 283 } 284 285 - (void)closeSheet:(ConstrainedWindowSheetInfo*)info 286 withAnimation:(BOOL)withAnimation { 287 if (![sheets_ containsObject:info]) 288 return; 289 290 [[NSNotificationCenter defaultCenter] 291 removeObserver:self 292 name:NSWindowDidResizeNotification 293 object:parentWindow_]; 294 295 [parentWindow_ removeChildWindow:[info overlayWindow]]; 296 [[info sheet] closeSheetWithAnimation:withAnimation]; 297 [[info overlayWindow] close]; 298 [sheets_ removeObject:info]; 299 } 300 301 @end 302