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).
Related
I'm working with an NSWindowController to implement a preferences window. Apple's documentation states that by default the controller and window aren't deallocated, because it's useful to not have to reload everything, which makes sense. But their documentation goes on to say that you can override that behavior, but not explain how.
Apple's Docs:
When a window is closed and it is part of a document-based
application, the document removes the window’s window
controller from its list of window controllers. This results
in the system deallocating the window controller and the
window, and possibly the NSDocument object itself. When a
window controller is not part of a document-based application,
closing the window does not by default result in the
deallocation of the window or window controller. This is the
desired behavior for a window controller that manages something
like an inspector; you shouldn’t have to load the nib file
again and re-create the objects the next time the user requests
the inspector.
If you want the closing of a window to make both
window and window controller go away when it isn’t
part of a document, your subclass of NSWindowController
can observe the NSWindowWillCloseNotification notification
or, as the window delegate, implement the windowWillClose: method.
I can't find anywhere that explains what to "implement" in the windowWillClose: method.
The window controller can be seen here:
https://github.com/gngrwzrd/gwpreferences/blob/master/GWPreferences/GWPreferences/GWPreferences/GWPrefsWindowController.m
Using the controller can be seen here:
https://github.com/gngrwzrd/gwpreferences/blob/master/GWPreferences/GWPreferences/GWAppDelegate.m - you can see in this code where I'm trying some bridge casting to try and force release objects but it doesn't work.
So the GWPrefsWindowController.dealloc method never gets called. Any ideas?
I understand this question is old, but for those who came here from google, the answer is quite simple.
As stated in the documentation, for non document base applications, you can simply:
Keep a reference for your NSWindowController wherever your are calling it. (In the example below it's referenced by myWindowController;
Make the class calling your NSWindowController implement the protocol NSWindowDelegate;
Release your Window Controller by setting it to nil on windowWillClose: method
To answer the question more precisely. When lazy instantiating your controller, set your class as the delegate:
-(IBAction)showMyWindowAction:(id)sender
{
// If my window controller is not nil
if (!myWindowController)
{
//instantiate it
myWindowController = [[MyWindowController alloc] initWithWindowNibName:#"myWindow"];
// set your class as delegate
[myWindowController setDelegate:self];
}
[myWindowController.window orderFront:self];
}
And then implement the windowWillClose: method from the NSWindowDelegate protocol
-(void)windowWillClose:(NSNotification *)notification
{
//Check if it's the right window that will close
if ([notification.object isEqualTo:myWindowController.window])
{
//Set your controller to nil
myWindowController = nil;
}
}
That's it, your window controller will now dealloc and since we are verifying if it's controller is nil before showing the window, everything will work!
I believe the reason why this is not implemented by default is because the initWithWindowNibName: is a somewhat heavy operation, and thus you have to think if dealloc'ing whatever is on your window will impact more or less than loading your window nib file.
I hope it helped
I would like to track each time a certain window appears (becomes visible to the user) in a OS X app. Where would be the most adequate place to call the tracker?
windowWillLoad, maybe?
I expected to find something like windowWillAppear but it seems I'm thinking too much iOS.
How about getting notification such as NSWindowDidBecomeMainNotification, By main I guess the one which is top most on screen directly visible by user.
see : Apple Documentation
Yes, one would expect that a window would notify its delegate or its controller with a windowWillAppear or windowDidAppear message, or post a documented notification like NSWindowDidAppearNotification. But alas, none of those exist. I filed a bug report with Apple and was given the advice to use a storyboard and a view controller instead. This is unhelpful in legacy apps that already use a bunch of window controllers and xibs.
You could subclass NSWindow and override orderWindow:relativeTo: to send a notification. Most, but not quite all, of the messages that make a window show itself ultimately go through this method, including orderBack:, orderFront:, makeKeyAndOrderFront:, and -[NSWindowController showWindow:]. But orderFrontRegardless does not go through orderWindow:relativeTo:, so you would also want to override that for completeness.
Another way to be notified is to make a subclass of NSViewController that controls some view that's always visible in the window. The view controller will receive viewWillAppear and viewDidAppear.
If you're subclassing NSWindow or NSViewController already for some other reason, either of these is a reasonable solution.
If you're not subclassing NSWindow already, and don't have an NSViewController subclass for a view that's always visible in the window, then another way is to use Cocoa bindings to connect the window's visible binding to a property one of your objects. For example, I have a custom NSWindowController subclass. I gave it a windowIsVisible property:
#interface MyWindowController ()
#property (nonatomic) BOOL windowIsVisible;
#end
and I implemented the accessors like this:
- (BOOL)windowIsVisible { return self.window.visible; }
- (void)setWindowIsVisible:(BOOL)windowIsVisible {
NSLog(#"window %# became %s", self.window, windowIsVisible ? "visible" : "hidden");
}
and in awakeFromNib, I bind the window's visible binding to the property like this:
- (void)awakeFromNib {
[super awakeFromNib];
[self.window bind:NSVisibleBinding toObject:self withKeyPath:NSStringFromSelector(#selector(windowIsVisible)) options:nil];
}
When the window becomes visible, the setWindowIsVisible: setter is called with an argument of YES. Note that if the whole app is hidden and reappears, the setter is called again, even though it wasn't called with argument NO when the app was hidden. So be careful not to assume the window was previously hidden.
Also, the binding might create a retain cycle, so you should probably unbind it when the window is closed, unless you want to keep the window and controller around. Note that the window does post NSWindowWillCloseNotification when it's closing, so you don't need any special magic to detect that.
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.
I have a document based cocoa application with an item in the application menu hooked up to an IBAction. Clicking the item needs to perform a task that uses an IBOutlet in the main nib file which is using another class, MyDocument. Creating 2 objects of the same class, one in each nib seems to not be working. How can I access the outlet?
Actions for menu items are often sent to the first responder so that whatever is currently selected can act on it.
It sounds like this action is something that works on the current document, then it should be implemented by the document. In this case have the menu send it's action to the first responder and then put the action method in the MyDocument class.
If the action you are trying to send is a custom one: in the Main Menu nib select the First Responder item, add your method name, then connect the menu item's selector to the action.
Read the Responders section of the Cocoa Event-Handling Guide for more info.
To summarize the above, in your NIB/XIB file, in interface builder make the connection to the First Responder object, not to Files Owner or anything else. You'll still be offered a lit of actions across potential first responders.
Cocoa then takes that selector and looks for it, starting with the NSView (if any) that's currently the first responder, then with the NSDocument that's currently in use, then with it's window controller etc etc all the way up to the Application delegate. The first object it checks that actually implements that method, it will use that object (after validating it with that same object).
So:
#interface MyDocumentTypeA : NSDocument {
}
-(void)myMenuAction:(id)sender;
-
#interface MyDocumntTypeB : NSDocument {
}
// -myMenuAction: not implemented here
-
#interface MyApplicationDelegate ... {
}
-(void)myMenuAction:(id)sender;
-
In Interface builder (or even programmatically), if you've linked the "action" of the menu item to a selector named "myMenuAction:" on the First Responder (which equates to not specifying a target when done programmatically), for the above two document subclasses the following will happen.
For MyDocumentTypeA, when the user selects that menu item, MyDocumentTypeA's -myMenuAction: will be invoked. Since MyDocumentTypeB does not implement this action, Cocoa will continue to look up the responder chain until it gets to your application delegate, which does implement it, so it will be invoked here instead.
If Cocoa finds no objects in the responder chain that implement the method, the menu item remains disabled.
There is a way how to do this, I've posted the answer in a similar thread: Access IBOutlet from other class (ObjC)
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.