aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AppKit/GTMWindowSheetController.h145
-rw-r--r--AppKit/GTMWindowSheetController.m575
-rw-r--r--AppKit/GTMWindowSheetControllerTest.m191
-rw-r--r--GTM.xcodeproj/project.pbxproj12
-rw-r--r--ReleaseNotes.txt2
5 files changed, 925 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:&param 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
diff --git a/GTM.xcodeproj/project.pbxproj b/GTM.xcodeproj/project.pbxproj
index 3259624..3ede967 100644
--- a/GTM.xcodeproj/project.pbxproj
+++ b/GTM.xcodeproj/project.pbxproj
@@ -59,6 +59,9 @@
7F511DFA0F4B0378009F41B6 /* GTMNSColor+Luminance.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F511DF40F4B0378009F41B6 /* GTMNSColor+Luminance.m */; };
7F511DFC0F4B0378009F41B6 /* GTMTheme.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F511DF60F4B0378009F41B6 /* GTMTheme.h */; settings = {ATTRIBUTES = (Public, ); }; };
7F511DFE0F4B0378009F41B6 /* GTMTheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F511DF80F4B0378009F41B6 /* GTMTheme.m */; };
+ 8207B89B0FEA7A9E008A527B /* GTMWindowSheetController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8207B8970FEA7A98008A527B /* GTMWindowSheetController.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 8207B89C0FEA7AA1008A527B /* GTMWindowSheetController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8207B8980FEA7A98008A527B /* GTMWindowSheetController.m */; };
+ 8207B89D0FEA7AA6008A527B /* GTMWindowSheetControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8207B8990FEA7A98008A527B /* GTMWindowSheetControllerTest.m */; };
84B91B8B0EA3CC2E0087500F /* GTMUnitTestingImage.10.6.0.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 84B91B890EA3CC2E0087500F /* GTMUnitTestingImage.10.6.0.tiff */; };
84B91B8C0EA3CC2E0087500F /* GTMUnitTestingWindow.10.6.0.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 84B91B8A0EA3CC2E0087500F /* GTMUnitTestingWindow.10.6.0.tiff */; };
8B0E65510FD80D5E00461C4A /* GTMHotKeyTextField.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8B0E653F0FD80D5E00461C4A /* GTMHotKeyTextField.strings */; };
@@ -390,6 +393,9 @@
7F511DF60F4B0378009F41B6 /* GTMTheme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMTheme.h; sourceTree = "<group>"; };
7F511DF70F4B0378009F41B6 /* GTMThemeTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMThemeTest.m; sourceTree = "<group>"; };
7F511DF80F4B0378009F41B6 /* GTMTheme.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMTheme.m; sourceTree = "<group>"; };
+ 8207B8970FEA7A98008A527B /* GTMWindowSheetController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMWindowSheetController.h; sourceTree = "<group>"; };
+ 8207B8980FEA7A98008A527B /* GTMWindowSheetController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMWindowSheetController.m; sourceTree = "<group>"; };
+ 8207B8990FEA7A98008A527B /* GTMWindowSheetControllerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMWindowSheetControllerTest.m; sourceTree = "<group>"; };
848269C80E9FF4BD006E6D9C /* DebugSnowLeopardOrLater.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = DebugSnowLeopardOrLater.xcconfig; sourceTree = "<group>"; };
848269C90E9FF4BD006E6D9C /* ReleaseSnowLeopardOrLater.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReleaseSnowLeopardOrLater.xcconfig; sourceTree = "<group>"; };
84B91B890EA3CC2E0087500F /* GTMUnitTestingImage.10.6.0.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = GTMUnitTestingImage.10.6.0.tiff; sourceTree = "<group>"; };
@@ -949,6 +955,9 @@
8B409BC50F94405A00DF540E /* GTMUILocalizerTest.m */,
8B409E8B0F952C2C00DF540E /* GTMUILocalizerTestWindow.xib */,
8B409F050F95341E00DF540E /* GTMUILocalizerTestView.xib */,
+ 8207B8970FEA7A98008A527B /* GTMWindowSheetController.h */,
+ 8207B8980FEA7A98008A527B /* GTMWindowSheetController.m */,
+ 8207B8990FEA7A98008A527B /* GTMWindowSheetControllerTest.m */,
F435E4840DC8F3DC0069CDE8 /* TestData */,
);
path = AppKit;
@@ -1228,6 +1237,7 @@
8BFE13B60FB0F2C0001BE894 /* GTMABAddressBook.h in Headers */,
8BD35B910FB22980009058F5 /* GTMNSScanner+JSON.h in Headers */,
8BF4D2E60FC7073A009ABC3F /* GTMGoogleSearch.h in Headers */,
+ 8207B89B0FEA7A9E008A527B /* GTMWindowSheetController.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1738,6 +1748,7 @@
8BFE13B70FB0F2C0001BE894 /* GTMABAddressBook.m in Sources */,
8BD35B920FB22980009058F5 /* GTMNSScanner+JSON.m in Sources */,
8BF4D2E70FC7073A009ABC3F /* GTMGoogleSearch.m in Sources */,
+ 8207B89C0FEA7AA1008A527B /* GTMWindowSheetController.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1765,6 +1776,7 @@
8BAA9EFC0F7C2AB500DF4F12 /* GTMNSWorkspace+RunningTest.m in Sources */,
8BAA9EFD0F7C2AB500DF4F12 /* GTMThemeTest.m in Sources */,
8B409BC60F94405A00DF540E /* GTMUILocalizerTest.m in Sources */,
+ 8207B89D0FEA7AA6008A527B /* GTMWindowSheetControllerTest.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt
index 9e6efd4..592a964 100644
--- a/ReleaseNotes.txt
+++ b/ReleaseNotes.txt
@@ -7,6 +7,8 @@ Discussion group: http://groups.google.com/group/google-toolbox-for-mac
Release ?.?.?
Changes since 1.5.1
+- Added GTMWindowSheetController for creating and controlling tab-modal sheets.
+
- Added GTMNSArray+Merge for merging one array into another with or without
a custom merging function, returning a new array with the merged contents.