Under what conditions can an NSWindowController's document change? - cocoa

My app observes the document property of its NSWindowController and performs some UI setup when it is set. Once it has been set, it would be difficult to rebuild the UI (for internal reasons) as the result of a change.
Once an NSWindowController has set its document property to an opened document, under what conditions will the system ever change that property to a new NSDocument instance (i.e., will the document ever be swapped out)? I've never observed it happening, but I can imagine features like versions or iCloud syncing causing the window controller's document to get swapped for a new document. However, the documentation on the NSWindowController lifecycle doesn't seem to touch on the issue.

It doesn't change once it has been set. NSWindowController gets its document by addWindowController method. Every new/opened document creates it's own windowController. Instance of document doesn't change with iCloud or revertChanges. It is up to you how to synchronise document with its views (redrawing).
/* Create the user interface for this document, but don't show it yet.
The default implementation of this method invokes [self windowNibName],
creates a new window controller using the resulting nib name (if it is not nil),
specifying this document as the nib file's owner, and then invokes [self addWindowController:theNewWindowController] to attach it. You can override
this method to use a custom subclass of NSWindowController or to create more
than one window controller right away. NSDocumentController invokes this method
when creating or opening new documents.
*/
// e.g. override
- (void)makeWindowControllers
{
if ([[self windowControllers] count] == 0) {
MainWindowController *controller = [[MainWindowController alloc] init];
[self addWindowController:controller];
}
}

Related

Setting up multiple NSWindowController objects and NSDocument

I'm new to the NSDocument architecture and am looking to set up multiple windows (and hence multiple NSWindowController objects) for a single document.
From what I understand, NSDocument was really created to work with a single window, and it seems that the ability to have multiple windows was shoehorned in later. For example, it seems that the NSDocument should always be the file's owner for any window's NIB files. But what if I wanted to separate the window controllers from the document?
For example, in the NSDocument subclass I am currently using the code:
- (void)makeWindowControllers {
[self setMyWindowController1:[[WindowControllerType1 alloc] initWithWindowNibName:#"MyWindow" owner:self]];
[self addWindowController:[self MyWindowController1]];
}
But the NIB file "MyWindow"'s file owner is set to the NSWindowController subclass (WindowControllerType1), NOT my NSDocument subclass. In this case, whenever I look to get the document by using [[NSDocumentController sharedDocumentController] currentDocument], this ALWAYS returns nil.
I figure this can be rectified if I set the NIB file's owner to the NSDocument subclass, but then all of my outlet links break, and I'm not sure how to link to the NSWindowController subclass (WindowControllerType1), as the typical course of action (as far as I can tell) is to make the NSDocument a window controller delegate as well, which I would like to avoid!
Any suggestions?
EDIT:
Let me clarify and add some new information. I am aware of Apple's position on using the WindowController's document property. However, as I plan of having a larger number of nested NSViews in each window, I want to avoid passing the document through a large chain of views in order to accomplish this.
The issue is not necessarily this chain. It is mostly that when the [[NSDocumentController sharedDocumentController] currentDocument] is ALWAYS nil, none of the "for free" features of NSDocument seem to work, such as undo/redo. This is the major issue that I need to resolve.
From what I understand, NSDocument was really created to work with a single window, and it seems that the ability to have multiple windows was shoehorned in later.
No, makeWindowControllers is available in OS X v10.0 and later.
But what if I wanted to separate the window controllers from the document?
The window controller owns the NIB.
Any suggestions?
Do
[self setMyWindowController1:[[WindowControllerType1 alloc] initWithWindowNibName:#"MyWindow"]].
NSWindowController has a property document which is set by addWindowController:.
Use document property of NSWindowController instead of currentDocument.

Where is the window outlet in an NSDocument

My app was converted from a non-document-based app to a document-based one. I did that by creating a subclass of NSDocument, called Document. I also created a Document.xib and set its "File's Owner" to Document.
Now in Document.xib, I can see there is a window outlet in its "File's Owner". I don't have a window outlet defined in Document. Where does it come from? I guess it is from the super class NSDocument, but I have no access to that variable in Document. What's up with this weird window outlet?
Have a look at the documentation for -[NSDocument setWindow:]
This method is invoked automatically during the loading of any nib for which this document is the file’s owner, if the file’s owner window outlet is connected in the nib. You should not invoke this method directly, and typically you would not override it either.
NSDocument doesn't deal with NSWindows directly, but it keeps a list of NSWindowControllers that you can access via the -[NSDocument windowControllers] method. My guess is that when setWindow: gets called, it wraps the window in a new NSWindowController and adds it to the list.
You should be able to access the window with something like this:
NSWindowController* controller = self.windowControllers.lastObject;
NSWindow* window = controller.window;
I just made a new project to test it, and this works:
- (void)windowControllerDidLoadNib:(NSWindowController *)aController {
[super windowControllerDidLoadNib:aController];
NSLog(#"%#", [self.windowControllers.lastObject window]);
}

NSDocument subclass instance apparently not in responder chain

I'm creating my first NSDocument based application. I'm able to create new documents, both from scratch and by importing legacy files.
This app will allow multiple windows per document, so I am overriding makeWindowControllers. This method is currently very simple:
- (void) makeWindowControllers
{
if (documentDatabase == nil) return;
DataSheetWindowController * dswc = [[DataSheetWindowController alloc] initWithDatabase:documentDatabase];
[self addWindowController: dswc];
}
The window appears as expected, however, the Save, Revert to Save, and other document enabled menus are disabled, as if the document was not in the responder chain.
As an experiment, I tried adding this method to my NSWindowController class:
- (void)saveDocument:(id)sender {
[[self document] saveDocument:sender];
}
With this method in place, the Save menu item is enabled, and selecting it causes the document's save methods to be invoked.
From reading the documentation and other questions on Stack Overflow, it's clear that something is wrong -- I should NOT have to put this method in the NSWindowController class. I'm sure I've overlooked something silly, but for the life of me I cannot figure out what it is, or any other mention of this problem here or elsewhere on the web.
Some additional information that may be useful -- in IB, the window's owner and delegate are set to the NSWindowController. I created a method to display the responder chain (see How to inspect the responder chain?) and the document was not listed. Here is the output of the responder chain (however, since NSDocument is not a subclass of NSResponder, I'm not sure if it is supposed to be listed anyway).
RESPONDER CHAIN:
<NSClipView: 0x102344350>
<NSScrollView: 0x102344480>
<NSView: 0x102345040>
<NSWindow: 0x10234e090>
Since the saveDocument method I put into the NSWindowController class does work, that indicates to me that the window controller does know that it is associated with the document.
So -- any thoughts as to why the document is behaving as if it is not in the responder chain?
Updated info: After setting up a new document, the initWithType method includes this temporary line to make sure that the document status is edited:
[self updateChangeCount:NSChangeDone];
I have verified that isDocumentEdited returns true.
I'm going to suggest that the solution is the one pointed to here:
https://stackoverflow.com/a/9349636/341994
In the nib containing the window that the window controller will be loading, the File's Owner proxy needs to be of the window controller's class (select the File's Owner proxy and examine the Identity inspector to confirm / configure that), and its window outlet must be hooked to the window and the window's delegate outlet must be hooked to the File's Owner proxy (select the File's Owner proxy and examine the Connections inspector to confirm that).

Document-based app doesn't restore documents with non-file URLs

I have an application based on NSDocument with an NSDocumentController subclass. My NSDocument works with both file URLs and URLs with a custom scheme which use a web service.
I handle much of the loading and saving using custom code, including -saveToURL:ofType:forSaveOperation:completionHandler:. +autosavesInPlace returns YES.
The problem I'm having: documents with the custom URL scheme aren't restored on startup. Documents with the file URL scheme are – both regular documents saved to files, and untitled documents which are autosaved.
After leaving open server-based documents and quitting the app, no NSDocument methods appear to be called on restart. In particular, none of the four initializers is called:
–init
–initWithContentsOfURL:ofType:error:
–initForURL:withContentsOfURL:ofType:error:
–initWithType:error:
The NSDocumentController method -reopenDocumentForURL:withContentsOfURL:display:completionHandler: is not called either.
How and when are documents' restorable state encoded? How and when are they decoded?
NSDocument is responsible for encoding its restorable state in -encodeRestorableStateWithCoder:, and NSDocumentController is responsible for decoding documents' restorable state and reopening the documents in +restoreWindowWithIdentifier:state:completionHandler:. Refer to the helpful comments in NSDocumentRestoration.h.
When NSDocument encodes the URL, it appears to use the bookmark methods of NSURL. The problem is that these methods only work with file-system URLs. (It's possible non-file URLs will encode, but they will not properly decode.)
To fix the problem, override the encoding of NSDocument instances which use the custom scheme, and likewise, the decoding of those documents.
NSDocument subclass:
- (void) encodeRestorableStateWithCoder:(NSCoder *) coder {
if ([self.fileURL.scheme isEqualToString:#"customscheme"])
[coder encodeObject:self.fileURL forKey:#"MyDocumentAutoreopenURL"];
else
[super encodeRestorableStateWithCoder:coder];
}
NSDocumentController subclass:
+ (void) restoreWindowWithIdentifier:(NSString *) identifier
state:(NSCoder *) state
completionHandler:(void (^)(NSWindow *, NSError *)) completionHandler {
NSURL *autoreopenURL = [state decodeObjectForKey:#"MyDocumentAutoreopenURL"];
if (autoreopenURL) {
[[self sharedDocumentController]
reopenDocumentForURL:autoreopenURL
withContentsOfURL:autoreopenURL
display:NO
completionHandler:^(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error) {
NSWindow *resultWindow = nil;
if (!documentWasAlreadyOpen) {
if (![[document windowControllers] count])
[document makeWindowControllers];
if (1 == document.windowControllers.count)
resultWindow = [[document.windowControllers objectAtIndex:0] window];
else {
for (NSWindowController *wc in document.windowControllers)
if ([wc.window.identifier isEqual:identifier]) {
resultWindow = wc.window;
break;
}
}
}
completionHandler(resultWindow, error);
}
];
} else
[super restoreWindowWithIdentifier:identifier
state:state
completionHandler:completionHandler];
}
The behavior or the completion handler follows from Apple's method comment in NSDocumentRestoration.h and should be roughly the same as super's.
Window state encoding is enabled by two methods on NSWindow. Calling setRestorable: on the window marks it as one that can be saved and restored on relaunch, and then calling setRestorationClass: lets you specify a class that will handle recreating that saved window.
By default, AppKit sets NSDocumentController as the restoration class of windows controlled by NSDocument objects. The actual restoration is done by calling the method +restoreWindowWithIdentifier:state:completionHandler:, defined by the NSWindowRestoration protocol. For documents, NSDocumentController implements that method and recreates the NSDocument object based on the state encoded in the NSCoder instance passed into the method.
So, theoretically, if you were to subclass NSDocumentController and override that method, that would give you an opportunity to restore documents saved by the state restoration mechanism. However, as far as I know, the keys used by NSDocumentController to store state are not documented anywhere, so I don't think there would be a reliable way to restore directly from the state that NSDocumentController stores itself.
To support this, you would probably need to encode the entire state for the document yourself, by implementing -encodeRestorableStateWithCoder: on the NSWindow being encoded, and/or implement the window:willEncodeRestorableState: delegate method for the window. Both of those methods pass you an NSCoder instance you can use to encode your state. That's where you would encode your custom-schemed URL, along with any other associated data you need to save/restore your state. You would then decode that state in the restoreWindowWithIdentifier:state:completionHandler: method.
Since you'll have some documents with regular file URLs and some with your custom URLs, I would approach that by creating a separate class responsible for decoding the document state, and set that as the restoration class only for your documents with custom URLs, leaving NSDocumentController to handle the documents withe file URLs for you.

In NSWindowController subclass, [self document] returns null

I am using a custom subclass of NSDocument and a custom subclass of NSWindowController. The problem is that I cannot reference my custom document from my custom window controller.
In IB, in the TKDocument NIB I have File's Owner set to TKWindowController.
In my TKDocument subclass I have:
- (void) makeWindowControllers {
TKWindowController *controller = [[TKWindowController alloc] init];
[self addWindowController:controller];
}
Then in my TKWindowController subclass I overrode setDocument to make sure it was being called:
- (void) setDocument(NSDocument *) document {
NSLog(#"setDocument:%#", document);
[super setDocument:document];
}
and then (again in TKWindowController) my action which references the document itself:
- (IBAction) plotClicked:(id) sender {
TKDocument *doc = [self document];
NSLog(#"plotClicked %#", doc);
}
The NSLog in setDocument outputs the string returned by my [TKDocument description] override as I'd expect; I only put it there to see if it was being called. However, doc in plotClicked is null.
What might I have done wrong?
EDIT: I believe the problem is to do with NIBs. My Document has its own NIB with File's Owner set to the custom controller as mentioned above. The plotClicked action is fired from a menu item in MainMenu.xib. I believe it's hitting a new instance of the controller which isn't associated with the current, active document.
So, how do I link the two? My question is really this: How do I obtain a handle to the current active document (or its windowcontroller) from MainMenu.xib?
Thanks
My Document has its own NIB with File's Owner set to the custom controller as mentioned above.
The File's Owner of a document nib should be the document. Consider that suspect #1.
The plotClicked action is fired from a menu item in MainMenu.xib. I believe it's hitting a new instance of the controller which isn't associated with the current, active document.
Did you put a window controller inside your main menu nib? If not, then that isn't the problem, since you must have wired up your plotClicked: menu item to the First Responder, and the window controller and its document will be in the responder chain.
If you did, then there's the solution: delete the window controller from the MainMenu nib and hook up your menu item to the First Responder, so that the action message goes down the responder chain, which will enable it to hit the document or window controller.
How do I obtain a handle to …?
The only Handles on the Mac come from Carbon; those Handles do not exist in Cocoa.
init is not a designated initializer of NSWindowController. You want one of these: – initWithWindow:, – initWithWindowNibName:, – initWithWindowNibName:owner:, or – initWithWindowNibPath:owner:.
Also, from the docs:
In your class’s initialization method,
be sure to invoke on super either one
of the initWithWindowNibName:...
initializers or the initWithWindow:
initializer. Which one depends on
whether the window object originates
in a nib file or is programmatically
created.

Resources