Usually when "window" and "document" are used in the same sentence, the
answer is to use NSWindowController
to manage the window.
Unfortunately there's not an obvious place in the NSDocument
/
NSWindowController
to put the code to update a shared
window when the current document changes.
Luckily NSWindowController
does most of the work for
us, so just a little glue here and there will get the shared inspector
behavior working.
NSWindowController
is one of the classes that make up
Cocoa's document architecture, along with NSDocument
and
NSDocumentController
. For an important class, there's
surprisingly little written about it. The CocoaDev wiki has a number of
pages on NSWindowController
, but the books I have only
have a page or two that cover NSWndowController
. Usually
it's along the lines of "make a controller, hook up the window outlet,
and then tell it to load. OK, Moving on to our next topic..." Most
of the discussion is along the lines of "here is how to make a
document that has multiple windows" rather than "here is how to share
a window amongst multiple documents".
While browsing CocoaDev, I came across MakingInspectorWindows, which has all the information needed to make an inspector. The trick is to use NSWindowController to load and manage the overview window. Then have the document class post a notification whenever a new document window comes to the front. The overview window controller thingie then listens for these notifications, and when it gets a notification, the controller updates the overview panel appropriately.
// BWGraphDocument.h -- primary document class holding the chart data #import <Cocoa/Cocoa.h> extern NSString *BWGraphDocument_DocumentDeactivateNotification; extern NSString *BWGraphDocument_DocumentActivateNotification; @interface BWGraphDocument : NSDocument { // ... blah } // ... more blah @end // BWGraphDocumentNow we just need to figure out when to post these notifications.
NSWindow
has some handy notifications for knowing when
windows has come to the front
(NSWindowDidBecomeMainNotification
) and when it's gone
back down the window stack
(NSWindowDidResignMainNotification
).
We could have the document (or the inspector for that matter) Listen
to all of these NSWindow
notifications whizzing past, and
have it sift out which notifications pertain to the document windows.
That's kind of a pain, especially because there's no one-stop-shopping
for getting the document's window.
Luckily, the main document window has its delegate set to be the document that it is associated with. When a window posts one of those notifications, it also invokes similarly named methods on the delegate. This is perfect for our needs. The document class just needs to implement these methods to track the active/inactive transitions of itse window, and it won't be distracted by any other window's notifications.
Here is a helper function (puts the notification code in one place), along with the delegate method implementations
- (void) postNotification: (NSString *) notificationName { NSNotificationCenter *center; center = [NSNotificationCenter defaultCenter]; [center postNotificationName: notificationName object: self]; } // postNotification - (void) windowDidBecomeMain: (NSNotification *) notification { [self postNotification: BWGraphDocument_DocumentActivateNotification]; } // windowDidBecomeMain - (void) windowDidResignMain: (NSNotification *) notification { [self postNotification: BWGraphDocument_DocumentDeactivateNotification]; } // windowDidResignMain - (void) windowWillClose: (NSNotification *) notification { [self postNotification: BWGraphDocument_DocumentDeactivateNotification]; } // windowDidClose
IBOutlets
,
NSArrayControllers
, and other assorted marklar in a
window controller subclass. Then provide an API so that users of the
window can give it the objects to manipulate (whether handing off an
object, or setting up bindings). I've seen suggestions that go to the
extreme of moving all of your document's UI logic into window
controllers and leave the document there just to handle file I/O. I
don't go to quite that extreme.
So, for each of my different palette windows in the app, each of them
has their own NSWindowController subclass (I currently have four, for
different kinds of inspectors). I've got a lot of this code in a
common base class (BWWindowController
) and have my
palette window controllers inherit from it. There are some reflexive
attribute methods (meaning some things the subclasses implement to
supply some of the more generic code with specific details) that the
subclasses implement. Those are peripheral details, and so I'll show
things generally hardwired for a single kind of palette window. But
it is pretty easy to refactor and genericize stuff.
Here is the interface for the panner window controller class:
// BWPannerWindowController.h -- controls the panner window #import <Cocoa/Cocoa.h> @class BWPannerView; @interface BWPannerWindowController : NSWindowController { IBOutlet BWPannerView *pannerView; } + (BWPannerWindowController *) sharedController; - (void) show; @end // BWPannerWindowControllerIts public API is
+sharedController
, which returns a
shared instance (the Singleton pattern for folks who like that kind of
stuff). There's nothing that prevents you from having multiple
inspector windows up, but in this case a Singleton makes sense because
a design requirement is that there is just one panner window open at a time.
There's also a -show
method, which is what clients call
when handling actions like "show panner window". Typical
usage by clients is:
// from the application controller class - (IBAction) showPannerWindow: (id) sender { [[BWPannerWindowController sharedController] show]; } // showPannerWindowThere's also an
IBOutlet
in the
BWPannerWindowController
class that has a reference to a
BWPannerView
, which is an NSView
subclass
that shows the contents of an NSScrollView
and lets the
user scroll it around.
Now for the implementation. The code starts out like you'd expect, including the necessary header files, and declaring a static pointer to point to the shared instance.
// BWPannerWindowController.m #import "BWPannerWindowController.h" #import "BWGraphDocument.h" #import "BWPannerView.h" static BWPannerWindowController *g_controller; @implementation BWPannerWindowControllerThe first method is
+sharedController
, which uses
NSWindow's initWithWindowNibName
to actually load the nib
file, which lives in the application bundle. The newly created
controller gets registered with the default notification center to
receive the two notifications that will be posted from the document.
+ (BWPannerWindowController *) sharedController { if (g_controller == nil) { g_controller = [[BWPannerWindowController alloc] initWithWindowNibName: @"BWPannerWindow"]; NSNotificationCenter *center; center = [NSNotificationCenter defaultCenter]; [center addObserver: g_controller selector: @selector(documentActivateNotification:) name: BWGraphDocument_DocumentActivateNotification object: nil]; [center addObserver: g_controller selector: @selector(documentDeactivateNotification:) name: BWGraphDocument_DocumentDeactivateNotification object: nil]; } return (g_controller); } // sharedController
The cleanup code is pretty simple. Just unhook ourselves from the notification center.
- (void) dealloc { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center removeObserver: self name: nil object: nil]; [super dealloc]; } // dealloc
The notification code is pretty straightforward too. When a document posts the activate notification, the panner controller sets its document to be the document that posted the notification. If the deactivate notification comes around, it clears out the current document.
- (void) documentActivateNotification: (NSNotification *) notification { NSDocument *document = [notification object]; [self setDocument: document]; } // documentActivateNotification - (void) documentDeactivateNotification: (NSNotification *) notification { [self setDocument: nil]; } // documentDeactivateNotification
setDocument:
is where the real meat of the work happens,
reacting to the document change to update the panner. Everything else
is life-support for this method.
- (void) setDocument: (NSDocument *) document { [super setDocument: document]; NSScrollView *view; view = [document valueForKey: @"layerViewScrollView"]; [pannerView setScrollView: view]; } // setDocument
-setDocument:
extracts from the scrollview being used by
the document (yeah, this is kind of a nasty way to do it, but the app
is still in its early bootstrap phase). -setDocument:
then updates the pannerView in the palette window to use the new
scroll view. setDocument
is also an
NSWindowController
method, so let it have first crack at
doing its work with the document.
And finally, -show
is what makes the panner window
visible. It sets the current document so that the panner gets hooked
up if there's already a document visible.
- (void) show { [self setDocument: [[NSDocumentController sharedDocumentController] currentDocument]]; [self showWindow: self]; } // show
NSWindowController
happily handles
window placement for you, defaulting to cascading the managed window
with previously placed windows. That's not good behavior for a
floating palette. The user expects it to stay in the last place it
was put. I put the code to handle change the default placment
behavior into windowDidLoad
, which is the
NSWindowController
equivalent of
awakeFromNib
.
- (void) windowDidLoad { [super windowDidLoad]; [self setShouldCascadeWindows: NO]; [self setWindowFrameAutosaveName: @"pannerWindow"]; [self setDocument: [self document]]; } // windowDidLoad
setShouldCascadeWindows:
tells the window controller to
not cascade the windows. The autosave name set in Interface Builder
doesn't get used by NSWindowController
, so that needs to
be set explicitly. Lastly the document is set again, just to make
sure that the panner gets set up with the current document. (I forget
if it's really necessary here since it's already been done in the
show
method, but it doesn't seem to be hurting anything.)
The last bit of code is making a good title for the panner window. It
should reflect the name of the current document ("Overview of
Untitled" or "Overview of Bear's Picnic"), so you can tell at a glance
that the panner is going to affect the document you expect.
windowTitleForDocumentDisplayName:
is called by
NSWindowController
to get the name to show in the window:
- (NSString *) windowTitleForDocumentDisplayName: (NSString *) displayName { NSString *string; string = [NSString stringWithFormat: NSLocalizedString(@"Overview of %@", @""), displayName]; return (string); } // windowTitleForDocumentDisplayName
And that's pretty much it. I've used this same pattern with the other shared palette windows in my application, and it works very smoothly.