| // |
| // 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:¶m 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 |