blob: 8e2d13c3291ee556daa111bedb17a1f4512a1080 [file] [log] [blame]
//
// GTMWindowSheetController.m
//
// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//
#import "GTMWindowSheetController.h"
#import "GTMDefines.h"
#import "GTMTypeCasting.h"
@interface GTMWSCSheetInfo : NSObject {
@public
NSWindow* overlayWindow_;
// delegate data
GTM_WEAK id modalDelegate_;
SEL didEndSelector_;
void* contextInfo_;
// sheet info
CGFloat sheetAlpha_;
NSRect sheetFrame_; // relative to overlay window
BOOL sheetAutoresizesSubviews_;
}
@end
@implementation GTMWSCSheetInfo
@end
// The information about how to call up various AppKit-implemented sheets
struct GTMWSCSystemSheetInfo {
NSString* className_;
NSString* methodSignature_;
NSUInteger modalForWindowIndex_;
NSUInteger modalDelegateIndex_;
NSUInteger didEndSelectorIndex_;
NSUInteger contextInfoIndex_;
// Callbacks invariably take three parameters. The first is always an id, the
// third always a void*, but the second can be a BOOL (8 bits), an int (32
// bits), or an id or NSInteger (64 bits in 64 bit mode). This is the size of
// the argument in 64-bit mode.
NSUInteger arg1OfEndSelectorSize_;
};
@interface GTMWindowSheetController (PrivateMethods)
- (void)beginSystemSheet:(id)systemSheet
withInfo:(const struct GTMWSCSystemSheetInfo*)info
modalForView:(NSView*)view
withParameters:(NSArray*)params;
- (const struct GTMWSCSystemSheetInfo*)infoForSheet:(id)systemSheet;
- (void)notificationHappened:(NSNotification*)notification;
- (void)viewDidChangeSize:(NSView*)view;
- (NSRect)screenFrameOfView:(NSView*)view;
- (void)sheetDidEnd:(id)sheet
returnCode8:(char)returnCode
contextInfo:(void*)contextInfo;
- (void)sheetDidEnd:(id)sheet
returnCode32:(int)returnCode
contextInfo:(void*)contextInfo;
- (void)sheetDidEnd:(id)sheet
returnCode64:(NSInteger)returnCode
contextInfo:(void*)contextInfo;
- (void)sheetDidEnd:(id)sheet
returnCode:(NSInteger)returnCode
contextInfo:(void*)contextInfo
arg1Size:(int)size;
- (void)systemRequestsVisibilityForWindow:(NSWindow*)window;
- (NSRect)window:(NSWindow*)window
willPositionSheet:(NSWindow*)sheet
usingRect:(NSRect)defaultSheetRect;
@end
@interface GTMWSCOverlayWindow : NSWindow {
GTMWindowSheetController* sheetController_;
}
- (id)initWithContentRect:(NSRect)contentRect
sheetController:(GTMWindowSheetController*)sheetController;
- (void)makeKeyAndOrderFront:(id)sender;
@end
@implementation GTMWSCOverlayWindow
- (id)initWithContentRect:(NSRect)contentRect
sheetController:(GTMWindowSheetController*)sheetController {
self = [super initWithContentRect:contentRect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
if (self != nil) {
sheetController_ = sheetController;
[self setOpaque:NO];
[self setBackgroundColor:[NSColor clearColor]];
[self setIgnoresMouseEvents:NO];
}
return self;
}
- (void)makeKeyAndOrderFront:(id)sender {
[sheetController_ systemRequestsVisibilityForWindow:self];
}
@end
@implementation GTMWindowSheetController
- (id)initWithWindow:(NSWindow*)window
delegate:(id <GTMWindowSheetControllerDelegate>)delegate {
self = [super init];
if (self != nil) {
window_ = window;
delegate_ = delegate;
sheets_ = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)finalize {
_GTMDevAssert([sheets_ count] == 0,
@"Finalizing a controller with sheets still active!");
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super finalize];
}
- (void)dealloc {
_GTMDevAssert([sheets_ count] == 0,
@"Deallocing a controller with sheets still active!");
[[NSNotificationCenter defaultCenter] removeObserver:self];
[sheets_ release];
[super dealloc];
}
- (void)beginSheet:(NSWindow*)sheet
modalForView:(NSView*)view
modalDelegate:(id)modalDelegate
didEndSelector:(SEL)didEndSelector
contextInfo:(void*)contextInfo {
NSArray* params =
[NSArray arrayWithObjects:sheet,
[NSNull null],
modalDelegate,
[NSValue valueWithPointer:didEndSelector],
[NSValue valueWithPointer:contextInfo],
nil];
[self beginSystemSheet:[NSApplication sharedApplication]
modalForView:view
withParameters:params];
}
- (void)beginSystemSheet:(id)systemSheet
modalForView:(NSView*)view
withParameters:(NSArray*)params {
const struct GTMWSCSystemSheetInfo* info = [self infoForSheet:systemSheet];
if (info) {
[self beginSystemSheet:systemSheet
withInfo:info
modalForView:view
withParameters:params];
} // else already logged
}
- (BOOL)isSheetAttachedToView:(NSView*)view {
NSValue* viewValue = [NSValue valueWithNonretainedObject:view];
return [sheets_ objectForKey:viewValue] != nil;
}
- (NSArray*)viewsWithAttachedSheets {
NSMutableArray* views = [NSMutableArray array];
NSValue* key;
GTM_FOREACH_KEY(key, sheets_) {
[views addObject:[key nonretainedObjectValue]];
}
return views;
}
- (void)setActiveView:(NSView*)view {
// Hide old sheet
NSValue* oldViewValue = [NSValue valueWithNonretainedObject:activeView_];
GTMWSCSheetInfo* oldSheetInfo = [sheets_ objectForKey:oldViewValue];
if (oldSheetInfo) {
NSWindow* overlayWindow = oldSheetInfo->overlayWindow_;
_GTMDevAssert(overlayWindow, @"Old sheet info has no overlay window");
NSWindow* sheetWindow = [overlayWindow attachedSheet];
_GTMDevAssert(sheetWindow, @"Old sheet info has no active sheet");
// Why do we hide things this way?
// - Keeping it local but alpha 0 means we get good Expose behavior
// - Resizing it to 0 means we get no blurring effect left over
oldSheetInfo->sheetAlpha_ = [sheetWindow alphaValue];
[sheetWindow setAlphaValue:(CGFloat)0.0];
oldSheetInfo->sheetAutoresizesSubviews_ =
[[sheetWindow contentView] autoresizesSubviews];
[[sheetWindow contentView] setAutoresizesSubviews:NO];
NSRect overlayFrame = [overlayWindow frame];
oldSheetInfo->sheetFrame_ = [sheetWindow frame];
oldSheetInfo->sheetFrame_.origin.x -= overlayFrame.origin.x;
oldSheetInfo->sheetFrame_.origin.y -= overlayFrame.origin.y;
[sheetWindow setFrame:NSZeroRect display:NO];
[overlayWindow setIgnoresMouseEvents:YES];
// Make sure the now invisible sheet doesn't keep keyboard focus
[[overlayWindow parentWindow] makeKeyWindow];
}
activeView_ = view;
// Show new sheet
NSValue* newViewValue = [NSValue valueWithNonretainedObject:view];
GTMWSCSheetInfo* newSheetInfo = [sheets_ objectForKey:newViewValue];
if (newSheetInfo) {
NSWindow* overlayWindow = newSheetInfo->overlayWindow_;
_GTMDevAssert(overlayWindow, @"New sheet info has no overlay window");
NSWindow* sheetWindow = [overlayWindow attachedSheet];
_GTMDevAssert(sheetWindow, @"New sheet info has no active sheet");
[overlayWindow setIgnoresMouseEvents:NO];
NSRect overlayFrame = [overlayWindow frame];
newSheetInfo->sheetFrame_.origin.x += overlayFrame.origin.x;
newSheetInfo->sheetFrame_.origin.y += overlayFrame.origin.y;
[sheetWindow setFrame:newSheetInfo->sheetFrame_ display:NO];
[[sheetWindow contentView]
setAutoresizesSubviews:newSheetInfo->sheetAutoresizesSubviews_];
[sheetWindow setAlphaValue:newSheetInfo->sheetAlpha_];
[self viewDidChangeSize:view];
[overlayWindow makeKeyWindow];
}
}
@end
@implementation GTMWindowSheetController (PrivateMethods)
- (void)beginSystemSheet:(id)systemSheet
withInfo:(const struct GTMWSCSystemSheetInfo*)info
modalForView:(NSView*)view
withParameters:(NSArray*)params {
_GTMDevAssert([view window] == window_,
@"Cannot show a sheet for a window for which we are not "
@"managing sheets");
_GTMDevAssert(![self isSheetAttachedToView:view],
@"Cannot show another sheet for a view while already managing "
@"one");
_GTMDevAssert(info, @"Missing info for the type of sheet");
GTMWSCSheetInfo* sheetInfo = [[[GTMWSCSheetInfo alloc] init] autorelease];
sheetInfo->modalDelegate_ = [params objectAtIndex:info->modalDelegateIndex_];
sheetInfo->didEndSelector_ =
[[params objectAtIndex:info->didEndSelectorIndex_] pointerValue];
sheetInfo->contextInfo_ =
[[params objectAtIndex:info->contextInfoIndex_] pointerValue];
_GTMDevAssert([sheetInfo->modalDelegate_
respondsToSelector:sheetInfo->didEndSelector_],
@"Delegate does not respond to the specified selector");
[view setPostsFrameChangedNotifications:YES];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(notificationHappened:)
name:NSViewFrameDidChangeNotification
object:view];
sheetInfo->overlayWindow_ =
[[GTMWSCOverlayWindow alloc]
initWithContentRect:[self screenFrameOfView:view]
sheetController:self];
[sheets_ setObject:sheetInfo
forKey:[NSValue valueWithNonretainedObject:view]];
[window_ addChildWindow:sheetInfo->overlayWindow_
ordered:NSWindowAbove];
SEL methodSelector = NSSelectorFromString((NSString*)info->methodSignature_);
NSInvocation* invocation =
[NSInvocation invocationWithMethodSignature:
[systemSheet methodSignatureForSelector:methodSelector]];
[invocation setSelector:methodSelector];
for (NSUInteger i = 0; i < [params count]; ++i) {
// Remember that args 0 and 1 are the target and selector, thus the |i+2|s
if (i == info->modalForWindowIndex_) {
[invocation setArgument:&sheetInfo->overlayWindow_ atIndex:i+2];
} else if (i == info->modalDelegateIndex_) {
[invocation setArgument:&self atIndex:i+2];
} else if (i == info->didEndSelectorIndex_) {
if (info->arg1OfEndSelectorSize_ == 64)
[invocation setArgument:&@selector(sheetDidEnd:returnCode64:contextInfo:)
atIndex:i+2];
else if (info->arg1OfEndSelectorSize_ == 32)
[invocation setArgument:&@selector(sheetDidEnd:returnCode32:contextInfo:)
atIndex:i+2];
else if (info->arg1OfEndSelectorSize_ == 8)
[invocation setArgument:&@selector(sheetDidEnd:returnCode8:contextInfo:)
atIndex:i+2];
} else if (i == info->contextInfoIndex_) {
[invocation setArgument:&view atIndex:i+2];
} else {
id param = [params objectAtIndex:i];
if ([param isKindOfClass:[NSValue class]]) {
char buffer[16];
[param getValue:buffer];
[invocation setArgument:buffer atIndex:i+2];
} else {
[invocation setArgument:&param atIndex:i+2];
}
}
}
[invocation invokeWithTarget:systemSheet];
_GTMDevAssert(!activeView_ || activeView_ == view,
@"You have to call setActiveView:view before "
"calling beginSheet:modalForView:view");
activeView_ = view;
}
- (const struct GTMWSCSystemSheetInfo*)infoForSheet:(id)systemSheet {
static const struct GTMWSCSystemSheetInfo kGTMWSCSystemSheetInfoData[] =
{
{
@"ABIdentityPicker",
@"beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:",
0, 1, 2, 3, 64,
},
{
@"CBIdentityPicker",
@"runModalForWindow:modalDelegate:didEndSelector:contextInfo:",
0, 1, 2, 3, 64,
},
{
@"DRSetupPanel",
@"beginSetupSheetForWindow:modalDelegate:didEndSelector:contextInfo:",
0, 1, 2, 3, 32,
},
{
@"NSAlert",
@"beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:",
0, 1, 2, 3, 32,
},
{
@"NSApplication",
@"beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo:",
1, 2, 3, 4, 64,
},
{
@"IKFilterBrowserPanel",
@"beginSheetWithOptions:modalForWindow:modalDelegate:didEndSelector:contextInfo:",
1, 2, 3, 4, 32,
},
{
@"IKPictureTaker",
@"beginPictureTakerSheetForWindow:withDelegate:didEndSelector:contextInfo:",
0, 1, 2, 3, 64,
},
{
@"IOBluetoothDeviceSelectorController",
@"beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:",
0, 1, 2, 3, 32,
},
{
@"IOBluetoothObjectPushUIController",
@"beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:",
0, 1, 2, 3, 32,
},
{
@"IOBluetoothServiceBrowserController",
@"beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:",
0, 1, 2, 3, 32,
},
{
@"NSOpenPanel",
@"beginSheetForDirectory:file:types:modalForWindow:modalDelegate:didEndSelector:contextInfo:",
3, 4, 5, 6, 32,
},
{
@"NSPageLayout",
@"beginSheetWithPrintInfo:modalForWindow:delegate:didEndSelector:contextInfo:",
1, 2, 3, 4, 32,
},
{
@"NSPrintOperation",
@"runOperationModalForWindow:delegate:didRunSelector:contextInfo:",
0, 1, 2, 3, 8,
},
{
@"NSPrintPanel",
@"beginSheetWithPrintInfo:modalForWindow:delegate:didEndSelector:contextInfo:",
1, 2, 3, 4, 32,
},
{
@"NSSavePanel",
@"beginSheetForDirectory:file:modalForWindow:modalDelegate:didEndSelector:contextInfo:",
2, 3, 4, 5, 32,
},
{
@"SFCertificatePanel",
@"beginSheetForWindow:modalDelegate:didEndSelector:contextInfo:certificates:showGroup:",
0, 1, 2, 3, 32,
},
{
@"SFCertificateTrustPanel",
@"beginSheetForWindow:modalDelegate:didEndSelector:contextInfo:trust:message:",
0, 1, 2, 3, 32,
},
{
@"SFChooseIdentityPanel",
@"beginSheetForWindow:modalDelegate:didEndSelector:contextInfo:identities:message:",
0, 1, 2, 3, 32,
},
{
@"SFKeychainSettingsPanel",
@"beginSheetForWindow:modalDelegate:didEndSelector:contextInfo:settings:keychain:",
0, 1, 2, 3, 32,
},
{
@"SFKeychainSavePanel",
@"beginSheetForDirectory:file:modalForWindow:modalDelegate:didEndSelector:contextInfo:",
2, 3, 4, 5, 32,
},
};
static const size_t kGTMWSCSystemSheetInfoDataSize =
sizeof(kGTMWSCSystemSheetInfoData)/sizeof(kGTMWSCSystemSheetInfoData[0]);
for (size_t i = 0; i < kGTMWSCSystemSheetInfoDataSize; ++i) {
Class testClass =
NSClassFromString(kGTMWSCSystemSheetInfoData[i].className_);
if (testClass && [systemSheet isKindOfClass:testClass]) {
return &kGTMWSCSystemSheetInfoData[i];
}
}
_GTMDevLog(@"Failed to find info for sheet of type %@", [systemSheet class]);
return nil;
}
- (void)notificationHappened:(NSNotification*)notification {
NSView *view = GTM_STATIC_CAST(NSView, [notification object]);
[self viewDidChangeSize:view];
}
- (void)viewDidChangeSize:(NSView*)view {
GTMWSCSheetInfo* sheetInfo =
[sheets_ objectForKey:[NSValue valueWithNonretainedObject:view]];
if (!sheetInfo)
return;
if (view != activeView_)
return;
NSWindow* overlayWindow = sheetInfo->overlayWindow_;
if (!overlayWindow)
return;
[overlayWindow setFrame:[self screenFrameOfView:view] display:YES];
[[overlayWindow attachedSheet] makeKeyWindow];
}
- (NSRect)screenFrameOfView:(NSView*)view {
NSRect viewFrame = [view convertRect:[view bounds] toView:nil];
viewFrame.origin = [[view window] convertBaseToScreen:viewFrame.origin];
return viewFrame;
}
- (void)sheetDidEnd:(id)sheet
returnCode8:(char)returnCode
contextInfo:(void*)contextInfo {
[self sheetDidEnd:sheet
returnCode:returnCode
contextInfo:contextInfo
arg1Size:8];
}
- (void)sheetDidEnd:(id)sheet
returnCode32:(int)returnCode
contextInfo:(void*)contextInfo {
[self sheetDidEnd:sheet
returnCode:returnCode
contextInfo:contextInfo
arg1Size:32];
}
- (void)sheetDidEnd:(id)sheet
returnCode64:(NSInteger)returnCode
contextInfo:(void*)contextInfo {
[self sheetDidEnd:sheet
returnCode:returnCode
contextInfo:contextInfo
arg1Size:64];
}
- (void)sheetDidEnd:(id)sheet
returnCode:(NSInteger)returnCode
contextInfo:(void*)contextInfo
arg1Size:(int)size {
NSValue* viewKey = [NSValue valueWithNonretainedObject:(NSView*)contextInfo];
// Retain a reference to sheetInfo so we can use it after it is
// removed from sheets_.
GTMWSCSheetInfo* sheetInfo =
[[[sheets_ objectForKey:viewKey] retain] autorelease];
_GTMDevAssert(sheetInfo, @"Could not find information about the sheet that "
@"just ended");
_GTMDevAssert(size == 8 || size == 32 || size == 64,
@"Incorrect size information in the sheet entry; don't know "
@"how big the second parameter is");
// Can't turn off view's frame notifications as we don't know if someone else
// wants them.
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:NSViewFrameDidChangeNotification
object:contextInfo];
// We clean up the sheet before calling the callback so that the
// callback is free to fire another sheet if it so desires.
[window_ removeChildWindow:sheetInfo->overlayWindow_];
[sheetInfo->overlayWindow_ release];
[sheets_ removeObjectForKey:viewKey];
NSInvocation* invocation =
[NSInvocation invocationWithMethodSignature:
[sheetInfo->modalDelegate_
methodSignatureForSelector:sheetInfo->didEndSelector_]];
[invocation setSelector:sheetInfo->didEndSelector_];
// Remember that args 0 and 1 are the target and selector
[invocation setArgument:&sheet atIndex:2];
if (size == 64) {
[invocation setArgument:&returnCode atIndex:3];
} else if (size == 32) {
int shortReturnCode = (int)returnCode;
[invocation setArgument:&shortReturnCode atIndex:3];
} else if (size == 8) {
char charReturnCode = returnCode;
[invocation setArgument:&charReturnCode atIndex:3];
}
[invocation setArgument:&sheetInfo->contextInfo_ atIndex:4];
[invocation invokeWithTarget:sheetInfo->modalDelegate_];
}
- (void)systemRequestsVisibilityForWindow:(NSWindow*)window {
NSValue* key;
GTM_FOREACH_KEY(key, sheets_) {
GTMWSCSheetInfo* sheetInfo = [sheets_ objectForKey:key];
if (sheetInfo->overlayWindow_ == window) {
NSView* view = [key nonretainedObjectValue];
[delegate_ gtm_systemRequestsVisibilityForView:view];
}
}
}
- (NSRect)window:(NSWindow*)window
willPositionSheet:(NSWindow*)sheet
usingRect:(NSRect)defaultSheetRect {
// Ensure that the sheets come out of the very top of the overlay windows.
NSRect windowFrame = [window frame];
defaultSheetRect.origin.y = windowFrame.size.height;
return defaultSheetRect;
}
@end