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 // BWGraphDocument
Now 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
A bit of wisdom I've picked up and have been applying with some
success is "For every unique kind of window you have, you should have
one unique kind of window controller." Window controllers are really
handy places for isolating the "here is how you handle this kind of
window" details. Put your 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 // BWPannerWindowController
Its 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];
} // showPannerWindow
There'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.