diff options
Diffstat (limited to 'AppKit')
-rw-r--r-- | AppKit/GTMWindowSheetController.h | 145 | ||||
-rw-r--r-- | AppKit/GTMWindowSheetController.m | 575 | ||||
-rw-r--r-- | AppKit/GTMWindowSheetControllerTest.m | 191 |
3 files changed, 911 insertions, 0 deletions
diff --git a/AppKit/GTMWindowSheetController.h b/AppKit/GTMWindowSheetController.h new file mode 100644 index 0000000..558814e --- /dev/null +++ b/AppKit/GTMWindowSheetController.h @@ -0,0 +1,145 @@ +// +// GTMWindowSheetController.h +// +// 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 <Cocoa/Cocoa.h> + +// A class to manage multiple sheets for a window. Use it for tab-style +// interfaces, where each tab might need its own sheet. +// +// While Cocoa can send notifications for when views resize, it does not do so +// for views appearing/disappearing. The owner is responsible for calling +// -setActiveView: appropriately as the visible views change. +// +// Notes on usage: +// - Cocoa isn't used to sheets being (ab)used in the way we use them here and +// makes sure we know it by providing slight visual anomalies like showing the +// close box as disabled but not actually disabling it, and not showing +// shadows for sheets. That's something you'll have to live with. +// - YOU are responsible for making sure that all sheets are closed before +// the windows containing them closes. That means: +// - You MUST implement the window delegate method -windowShouldClose: for any +// window using this class. In it, call -viewsWithAttachedSheets to see if +// there are any views with sheets attached to them. If there are, switch to +// that view and do not allow the window to close. +// - You MUST implement GTMWindowSheetControllerDelegate's method +// -gtm_systemRequestsVisibilityForView:. When that method is called, the +// system is trying to quit but realizes that there is a sheet on a window +// that prevents it from doing so. In such a case, switch to that view. +// (The quit is already prevented from happening so you don't need to worry +// about it.) +// - You MUST implement the application delegate method +// -applicationShouldTerminate:. In it, for every window that might have a +// sheet, call -viewsWithAttachedSheets to see if there are any views with +// sheets attached to them. If there are, switch to that view and do not +// allow the application to quit. +// I hope you see a pattern here. + +@protocol GTMWindowSheetControllerDelegate +- (void)gtm_systemRequestsVisibilityForView:(NSView*)view; +@end + + +@interface GTMWindowSheetController : NSObject { + @private + __weak NSWindow* window_; + __weak NSView* activeView_; + __weak id <GTMWindowSheetControllerDelegate> delegate_; + + NSMutableDictionary* sheets_; // NSValue*(NSView*) -> SheetInfo* +} + +// Initializes the class for use. +// +// Args: +// window: The window for which to manage sheets. All views must be +// contained by this window. +// delegate: The delegate for this sheet controller. +// +- (id)initWithWindow:(NSWindow*)window + delegate:(id <GTMWindowSheetControllerDelegate>)delegate; + +// Starts a view modal session for a sheet. Intentionally similar to +// -[NSApplication beginSheet:modalForWindow:modalDelegate:didEndSelector: +// contextInfo:]. +// +// Args: +// sheet: The window object representing the sheet you want to +// display. +// view: The view object to which you want to attach the sheet. +// modalDelegate: The delegate object that defines your didEndSelector +// method. +// didEndSelector: The method on the modalDelegate that will be called when +// the sheet’s modal session has ended. This method must be +// defined on the object in the modalDelegate parameter and +// have the following signature: +// - (void)sheetDidEnd:(NSWindow *)sheet +// returnCode:(NSInteger)returnCode +// contextInfo:(void *)contextInfo; +// contextInfo: A pointer to the context info you want passed to the +// didEndSelector method when the sheet’s modal session ends. +// +- (void)beginSheet:(NSWindow*)sheet + modalForView:(NSView*)view + modalDelegate:(id)modalDelegate + didEndSelector:(SEL)didEndSelector + contextInfo:(void *)contextInfo; + +// Starts a view modal session for a system sheet. Just about any AppKit class +// that has an instance method named something like -beginSheetModalForWindow... +// will work with this method. +// +// Args: +// systemSheet: The object that will show a sheet when triggered +// appropriately. +// view: The view object to which you want to attach the sheet. +// modalDelegate: The delegate object that defines your didEndSelector +// method. +// params: The parameters of the -beginSheetModalForWindow... selector. +// For the parameter named "window", insert [NSNull null] into +// the array instead. +// +- (void)beginSystemSheet:(id)systemSheet + modalForView:(NSView*)view + withParameters:(NSArray*)params; + +// Returns a BOOL value indicating whether the specified view has a sheet +// attached to it (hidden or not). +// +// Args: +// view: The view object to which a sheet might be attached. +// +// Returns: +// Whether or not a sheet is indeed attached to that view. +// +- (BOOL)isSheetAttachedToView:(NSView*)view; + +// Returns a list of views that have sheets attached (hidden or not). +// +// Returns: +// An array of views that have sheets. +// +- (NSArray*)viewsWithAttachedSheets; + +// Sets the specified view as active. The sheet (if there is one) for the active +// view is shown; sheets for all other views are hidden. +// +// Args: +// view: The view object to which a sheet is attached. +// +- (void)setActiveView:(NSView*)view; +@end diff --git a/AppKit/GTMWindowSheetController.m b/AppKit/GTMWindowSheetController.m new file mode 100644 index 0000000..4307d05 --- /dev/null +++ b/AppKit/GTMWindowSheetController.m @@ -0,0 +1,575 @@ +// +// 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" + +@interface GTMWSCSheetInfo : NSObject { + @public + __weak NSWindow* overlayWindow_; + + // delegate data + __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]; + } + + 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]; + } +} + +@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]; + + 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", + @"ru32perationModalForWindow: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]); + + NSString* className = NSStringFromClass([systemSheet class]); + for (size_t i = 0; i < kGTMWSCSystemSheetInfoDataSize; ++i) + if ([kGTMWSCSystemSheetInfoData[i].className_ isEqualToString:className]) + return &kGTMWSCSystemSheetInfoData[i]; + + _GTMDevLog(@"Failed to find info for sheet of type %@", [systemSheet class]); + return nil; +} + +- (void)notificationHappened:(NSNotification*)notification { + [self viewDidChangeSize:[notification object]]; +} + +- (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 frame]; + viewFrame = [[view superview] convertRect:viewFrame 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]; + GTMWSCSheetInfo* sheetInfo = [sheets_ objectForKey:viewKey]; + _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]; + + 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_]; + + [window_ removeChildWindow:sheetInfo->overlayWindow_]; + [sheetInfo->overlayWindow_ release]; + + [sheets_ removeObjectForKey:viewKey]; +} + +- (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 diff --git a/AppKit/GTMWindowSheetControllerTest.m b/AppKit/GTMWindowSheetControllerTest.m new file mode 100644 index 0000000..67e32be --- /dev/null +++ b/AppKit/GTMWindowSheetControllerTest.m @@ -0,0 +1,191 @@ +// +// GTMWindowSheetControllerTest.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 "GTMSenTestCase.h" +#import "GTMWindowSheetController.h" +#import "GTMNSObject+UnitTesting.h" + +@interface GTMWindowSheetControllerTest : GTMTestCase + <GTMWindowSheetControllerDelegate> { + @private + GTMWindowSheetController *sheetController_; + BOOL didAlertClose_; + BOOL didSheetClose_; +} +@end + +@implementation GTMWindowSheetControllerTest + +- (void)testOpenTwoSheetsAndSwitch { + // Set up window + NSWindow *window = + [[[NSWindow alloc] initWithContentRect:NSMakeRect(100, 100, 600, 600) + styleMask:NSTitledWindowMask + backing:NSBackingStoreBuffered + defer:NO] autorelease]; + STAssertNotNil(window, @"Could not allocate window"); + NSTabView *tabView = + [[[NSTabView alloc] initWithFrame:NSMakeRect(10, 10, 580, 580)] + autorelease]; + STAssertNotNil(tabView, @"Could not allocate tab view"); + [[window contentView] addSubview:tabView]; + [tabView setDelegate:self]; + + NSTabViewItem *item1 = + [[[NSTabViewItem alloc] initWithIdentifier:@"one"] autorelease]; + [item1 setLabel:@"One"]; + NSTabViewItem *item2 = + [[[NSTabViewItem alloc] initWithIdentifier:@"two"] autorelease]; + [item2 setLabel:@"Two"]; + [tabView addTabViewItem:item1]; + [tabView addTabViewItem:item2]; + + sheetController_ = + [[[GTMWindowSheetController alloc] initWithWindow:window + delegate:self] autorelease]; + + STAssertFalse([sheetController_ isSheetAttachedToView: + [[tabView selectedTabViewItem] view]], + @"Sheet should not be attached to current view"); + STAssertEquals([[sheetController_ viewsWithAttachedSheets] count], + (NSUInteger)0, + @"Should have no views with sheets"); + + // Pop alert on first tab + NSAlert* alert = [[NSAlert alloc] init]; + + [alert setMessageText:@"Hell Has Broken Loose."]; + [alert setInformativeText:@"All hell has broken loose. You may want to run " + @"outside screaming and waving your arms around " + @"wildly."]; + + NSButton *alertButton = [alert addButtonWithTitle:@"OK"]; + + [sheetController_ beginSystemSheet:alert + modalForView:[item1 view] + withParameters:[NSArray arrayWithObjects: + [NSNull null], + self, + [NSValue valueWithPointer: + @selector(alertDidEnd:returnCode:context:)], + [NSValue valueWithPointer:nil], + nil]]; + didAlertClose_ = NO; + + STAssertTrue([sheetController_ isSheetAttachedToView: + [[tabView selectedTabViewItem] view]], + @"Sheet should be attached to current view"); + STAssertEquals([[sheetController_ viewsWithAttachedSheets] count], + (NSUInteger)1, + @"Should have one view with sheets"); + + [tabView selectTabViewItem:item2]; + + STAssertFalse([sheetController_ isSheetAttachedToView: + [[tabView selectedTabViewItem] view]], + @"Sheet should not be attached to current view"); + STAssertEquals([[sheetController_ viewsWithAttachedSheets] count], + (NSUInteger)1, + @"Should have one view with sheets"); + + // Pop sheet on second tab + NSPanel *sheet = + [[[NSPanel alloc] initWithContentRect:NSMakeRect(0, 0, 300, 200) + styleMask:NSTitledWindowMask + backing:NSBackingStoreBuffered + defer:NO] autorelease]; + + [sheetController_ beginSheet:sheet + modalForView:[item2 view] + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:context:) + contextInfo:nil]; + didSheetClose_ = NO; + + STAssertTrue([sheetController_ isSheetAttachedToView: + [[tabView selectedTabViewItem] view]], + @"Sheet should be attached to current view"); + STAssertEquals([[sheetController_ viewsWithAttachedSheets] count], + (NSUInteger)2, + @"Should have two views with sheets"); + + [tabView selectTabViewItem:item1]; + + STAssertTrue([sheetController_ isSheetAttachedToView: + [[tabView selectedTabViewItem] view]], + @"Sheet should be attached to current view"); + STAssertEquals([[sheetController_ viewsWithAttachedSheets] count], + (NSUInteger)2, + @"Should have two views with sheets"); + + // Close alert + [alertButton performClick:self]; + + STAssertFalse([sheetController_ isSheetAttachedToView: + [[tabView selectedTabViewItem] view]], + @"Sheet should not be attached to current view"); + STAssertEquals([[sheetController_ viewsWithAttachedSheets] count], + (NSUInteger)1, + @"Should have one view with sheets"); + STAssertTrue(didAlertClose_, @"Alert should have closed"); + + [tabView selectTabViewItem:item2]; + + STAssertTrue([sheetController_ isSheetAttachedToView: + [[tabView selectedTabViewItem] view]], + @"Sheet should be attached to current view"); + STAssertEquals([[sheetController_ viewsWithAttachedSheets] count], + (NSUInteger)1, + @"Should have one view with sheets"); + + // Close sheet + [[NSApplication sharedApplication] endSheet:sheet returnCode:NSOKButton]; + + STAssertFalse([sheetController_ isSheetAttachedToView: + [[tabView selectedTabViewItem] view]], + @"Sheet should not be attached to current view"); + STAssertEquals([[sheetController_ viewsWithAttachedSheets] count], + (NSUInteger)0, + @"Should have no views with sheets"); + STAssertTrue(didSheetClose_, @"Sheet should have closed"); +} + +- (void)alertDidEnd:(NSAlert *)alert + returnCode:(NSInteger)returnCode + context:(void *)context { + didAlertClose_ = YES; +} + +- (void)sheetDidEnd:(NSWindow *)sheet + returnCode:(NSInteger)returnCode + context:(void *)context { + didSheetClose_ = YES; + [sheet orderOut:self]; +} + +- (void)tabView:(NSTabView *)tabView +didSelectTabViewItem:(NSTabViewItem *)tabViewItem { + NSView* view = [tabViewItem view]; + [sheetController_ setActiveView:view]; +} + +- (void)gtm_systemRequestsVisibilityForView:(NSView*)view { + STAssertTrue(false, @"Shouldn't be called"); +} + +@end |