In NSWindowController subclass, [self document] returns null - cocoa

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.

Related

Under what conditions can an NSWindowController's document change?

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];
}
}

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).

Receive window notifications

I have an NSWindow set up in Interface Builder. I have set the class of File's Owner to my NSWindowController and linked the window property of the controller to my NSWindow.
My controller implements NSWindowDelegate.
Now, in my controller, I have added the following:
- (void)windowDidLoad
{
[super windowDidLoad];
[self.window setDelegate:self];
}
- (void)windowDidBecomeMain:(NSNotification *)notification
{
NSLog(#"Did become main.");
}
Still, -windowDidBecomeMain: isn't called. Does anyone know why this is?
EDIT:
Trying to show a window from AppDelegate on launch. The main nib (declared in Info.plist) contains a menu item only which is linked to the AppDelegate. In the application delegate, I show an icon on the status bar and when this icon is clicked, I display the menu from the main nib.
In the application delegate, I also want to display a window which should have a window controller assigned to take care of the logic.
I believe that when this works, I will receive my window notifications.
Now, the following code doesn't show the window and I can't figure out why.
DemoWindowController *dwc = [[DemoWindowController alloc] initWithWindowNibName:#"DemoWindowController"];
[dwc showWindow:self];
Note that self is the application delegate.
I suspect your problem is due to the fact that your window controller is not actually the object that is the nibs file owner.
When you change the class in interface builder you are telling it what outlets and actions are available (which is why you are able to drag to the window outlet) but you are still responsible for passing in this object yourself.
In the case of a non-document based application, you will have a main method which calls NSApplicationMain. What this does is basically look up and load the window nib that is specified in your info.plist file and pass the current NSApplication instance to this nib as the files owner (so even though you changed the class type to NSWindowController, the object being passed in is actually of type NSApplication).
The easiest way to fix your problem is to get rid of your window controller for now (as it isn't actually doing anything yet).
You should implement the -windowDidBecomeMain: method in your app delegate. Then Ctrl+drag from your window to your appDelegate to set it as the delegate of the window to get your notifications.
Update
To answer your question regarding the WindowController beware of the following two issues:
You are creating your window controller variable (dwc) in your applicationDidFinishLaunching: method. This is released the moment you leave the method taking your window with it. Create an instance variable to hold onto the window controller instead.
Ensure that your second window nib has its file owner set to NSWindowController (or your window controller type) and that its window outlet is connected to the window in the nib file.
Your window should now display.

Problem with multiple window/NIB cocoa application

I'm having a problem with my Cocoa app. I'm using my app delegate as a controller, and opening one window in a NIB file. Clicking a toolbar button opens another window from another NIB. Clicking save on this second window calls a method on the app delegate/controller. All this works fine.
The strange thing is that I can't figure out is that the app delegate points to one memory location when I click the toolbar button and to a different memory location after clicking save on the second window. It's as if a second app delegate/controller is being created, though stepping through the code doesn't give me any indication of that occurring.
Is there a better way to architect this type of application? Any idea of where I'm going wrong?
It sounds like you're creating a second instance of your AppController class in your window's nib file. You can't do that, each instance of an object in a nib file will be instantiated when a nib is unarchived at runtime. This means if you have an AppController instance in MainMenu.xib and also one your MyWindow.xib file, the AppController object will be alloced and initialized twice.
Normally the way you'd handle this is by using the responder chain. In your Window nib, you assign First Responder as the target of your actions. This means that when the action method is called, the app will ask the currently focused view/control (the one that has first responder status) if it responds to the method by calling the -respondsToSelector: method and passing in the action selector.
If the first responder doesn't respond to the method, the message travels up the responder chain until an object that does respond to the method is found. If no object responds to the method, the NSApplication instance handles it and calls NSBeep().
Just before the method is sent to the NSApplication instance, the application delegate is asked if it responds to the selection. In this case, if your AppController object is set as the application delegate, it will receive the message sent as the action from your object in the window nib.
If this isn't clear enough, it's worth reading the Event Handling guide
You don't have to use the responder chain. You can call a method on the application delegate by calling [[NSApp delegate] yourMethod]. You could also store a reference to the app controller by adding it as an instance variable to your NSWindowController object that loads the nib and setting it at creation time, like so:
- (id)initWithAppController:(id)aController
{
self=[super initWithWindowNibName:#"YourWindowNibName"];
if(self)
{
appController = [aController retain];
}
return self;
}
Your window controller can then call methods of your AppController directly.

Resources