How can I intercept the result of openDocument before NSDocument loads? - macos

I am writing a program which will load and process XML data. If the XML file contains a single XML 'Dictionary' then it will need to open an NSDocument window (so far, so good - I can do this!), but if the XML file contains an array of Dictionaries then it should open up a list window, from which the individual Dictionaries can be opened into an NSDocument.
Because File->Open sends an action to First Responder->openDocument before the document window opens, I think that the openDocument function is not part of NSDocument. I'd like, therefore, to be able to intercept the open function before it hands off to NSDocument - just to check if the document is one that I want to be opening as a document rather than as my natty list view.
If, on the other hand, openDocument is an NSDocument function, how can I quietly close the NSDocument window and hand the XML list to my list window without raising an error in this one scenario? Of course, I don't want to suppress errors altogether - because there may be legitimate reasons to raise an error (unreadable file, bad syntax etc)
I realise that what I'm trying to do is a little unorthodox - but hopefully its possible. Any ideas?

openDocument: is an instance method of NSDocumentController.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSDocumentController_Class/Reference/Reference.html#//apple_ref/doc/uid/20000030-openDocument_
You can subclass the NSDocumentController with your own. This will allow you to intercept openDocument:.
If you want to check the file before creating a document, you'll need to use NSOpenPanel for the open file dialog. Then call openDocumentWithContentsOfURL:display:completionHandler: when you want to create the document. If you don't want to create the document, you can trigger whatever you want to do instead.

Just add a function to handle openDocument to your app delegate
func openDocument(sender: AnyObject) {
print("openDocument got called")
}
run app and press cmd+o

Related

Make "Close All" (cmd+option+W) apply only to one type of document windows

I have an application that manages different types of NSDocument subclasses (along with matching NSWindow subclasses).
For instance, it's possible that the app has one window of type A open, and two windows of type B.
Now, if a window of type B is active, and the user chooses "Close All" or hits cmd+option+W, all my app's windows are sent the close message.
But I only want all of the active window type's windows closed instead, i.e. only the two type B, not the type A window. How do I accomplish this?
I currently have no explicit menu entry for "Close All". Instead, macOS provides that automagically. If there perhaps a way to intercept a "closeAll" message? Can't find one, though.
AppKit will add the Close All menu item if there isn't one. Add an alternate menu item item with key equivalent cmd+option+W below the Close menu and connect it to your own action method.
You might succeed with overriding your document's canClose(withDelegate:,shouldClose:,contextInfo:) to return whether a document should be closed.
If this doesn't behave the way you want, you can create a subclass of NSDocumentController (if you don't have one already). Details on how to do that vary, but usually you have main (menu) XIB or main Storyboard, which has a "Document Controller" object: set its class to your custom class.
Then override closeAllDocuments(withDelegate:,didCloseAllSelector:,contextInfo:) and implement your custom logic.
Note that you should detect whether your app is about to quit and then really do close all you documents (unless you really want to prevent the app quit, e.g. because a document is dirty).
After some digging I figured out where the auto-generated "Close All" menu item sends its action to: To the closeAll: selector of the application target:
Thus my solution is to subclass NSApplication and implement the handler there, which then simply closes all windows that are of the same type (this assumes that I use specific subclasses for my different types of windows):
- (IBAction)closeAll:(id)sender {
Class windowClass = self.keyWindow.class;
for (NSWindow *w in self.windows) {
if (windowClass == w.class) {
[w performClose:sender];
}
}
}
Caution: If you adopt this pattern be aware that:
The closeAll: selector is not documented nor mentioned in the header files, meaning that Apple might feel free to change in a future SDK, though I find that unlikely. It will probably not break anything if that happens, but instead your custom handler won't be called any more.
The code simply tells all windows to close, ignoring the fact that one might reject to be closed, e.g. by user interaction. In that case you may want to stop the loop instead of continuing to close more windows (though I know of no easy way to accomplish that).

How to create multiple windows using "command + n" in non document based application

Is there a way to create/enable having multiple windows using "command + n" in a non document based application? I want to have unlimited instance of that window (not actually unlimited, but might be 6-7 instances) using command + n
Or I have to create a document based app and port all my code in new project template is the only solution?
I can see the menu button for "New" is disabled right now.
A few ways to do this.
First connect the New menu item to an IBAction method.
Name the method whatever makes sense to you.
Next, you will want to add some kind of property to your controller ( app delegate for simplicity ) that is basically a window stack only storing a reference to each window or window controller. NSMutableArray should do nicely.
Now you can do the next part a few ways, but I would recommend creating an NSWindowController subclass with a nib/xib (especially if these windows will have the same basic things in them).
Do what you want in the nib file.
Now in your IBAction method, create a new instance of your window controller class, add it to your mutable array. Tell it to load its window.
You only have to decide if the controller should be removed from the stack and set to nil if its window is closed.
Many ways to handle that, and up to your design to know what is correct.
Try this :-
NSWindowController *yourWindow=[[[[yourWindowController alloc]init]retain]autorelease];
[yourWindow loadWindow];

Export NSDocument in Mac app

How do you export a NSDocument in one format into another NSDocument in another format?
I would like to implement the typical Export option in my document-based app. I'm not sure where I should put the format conversion code, and what is already provided by Cocoa.
All the writing options in NSDocument get a string parameter to specify the type of file that should be written. So in your dataOfType:error: or fileWrapperOfType:error: methods you should implement the conversion code for each file type you want to support.
To start your export operation you can use the method saveToURL:ofType:forSaveOperation:completionHandler: with the desired type and a save operation of NSSaveToOperation.
For more information on the methods you can override to support loading and saving document data take a look at this programming guide.
You can get the available types from the class method writableTypes or the instance method writableTypesForSaveOperation:, again with NSSaveToOperation.
The file types you want to support need to be declared in your Info.plist file.
If your NSDocument subclass supports in-place autosaving, and all writable types are also readable (as they should be), I would recommend to use the already provided type-conversion workflow, where the user should use "Duplicate" followed by "Save".
In this workflow, when the user "Duplicate" the document, it's written/copied to a temporary file (where autosaved files are saved) as an untitled document. When the user closes the document window, the app suggests her to save the document or delete it. Since the document has no permanent URL yet, an NSSavePanel will appear with an accessory view that lets the user to select the document type.
In this solution everything is already provided by Cocoa and you don't have to do anything to support a special "Export" functionality as the user can use "Duplicate" followed by "Save".
You only have to be able to save your document to all writable types from dataOfType:error: or in fileWrapperOfType:error: according to the typeName argument (as Sven said).
The advantage here is that the user has to choose the URL only when she closes the file (and chooses not to delete it) - and is compatible with the new workflow in document-based apps where the "save as" operation has been replaced by "duplicate" followed by "save".
Note that you also have to make sure that you can duplicate documents of non-writable documents (you can achieve that by copying the original file instead of using writeSafelyToURL:ofType:forSaveOperation:error:).

Add to the "Open Recent" menu an item that doesn't point to a file

Is there a way to add an item that doesn't point to a file that exists on the file system to the "Open Recent" menu?
In an application not based on NSDocument, I can add an item to the "Open Recent" submenu with the following code:
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL URLWithString:stringToFilePath]];
It works as documented, as long as the URL points to a file that exists on the file system.
If the url doesn't point to a file on the system, such as a web url, or a custom url scheme, nothing happens.
For example, this code has no effect, and produce no log during execution, even if my app handles the scheme used in the URL:
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL URLWithString:#"http://www.stackoverflow.com"]];
Update: someone found (a long time ago) a way to tweak this menu to have it show files whether they exist or not: http://lists.apple.com/archives/cocoa-dev/2007/Apr/msg00651.html
I successfully managed to subclass NSDocumentController, but my override of the method - (NSArray *)recentDocumentURLs is never called.
It's not very surprising, as the doc says:
This method is not a good one to
override since the internals of
NSDocumentController do not generally
use it.
But the doc doesn't say what to use instead and the poster didn't give more detail. Any idea?
If there is no solution, on workaround would be to rewrite the entire menu from scratch.
If possible, I would prefer to avoid that, for all the stuff I get for free (like when you have two items with the same name, it displays the parent directory as well to help differentiate them).
It looks like you'll probably have to create your own menu and maintain your own separate list. This menu automatically excludes files that don't exist.
I believe this is also true of files on removable media that is absent (ie, if the media comes back, the I believe the file is once again available in the list if it hasn't been pushed off by more recent items).

NSDocument Subclass not closed by NSWindowController?

Okay, I'm fairly new to Cocoa and Objective-C, and to OOP in general.
As background, I'm working on an extensible editor that stores the user's documents in a package. This of course required some "fun" to get around some issues with NSFileWrapper (i.e. a somewhat sneaky writing and loading process to avoid making NSFileWrappers for every single document within the bundle). The solution I arrived at was to essentially treat my NSDocument subclass as just a shell -- use it to make the folder for the bundle, and then pass off writing the actual content of the document to other methods.
Unfortunately, at some point I seem to have completely screwed the pooch. I don't know how this happened, but closing the document window no longer releases the document. The document object doesn't seem to receive a "close" message -- or any related messages -- even though the window closes successfully.
The end result is that if I start my app, create a new document, save it, then close it, and try to reopen it, the document window never appears. With some creative subclassing and NSLogging, I managed to figure out that the document object was still in memory, and still attached to the NSDocumentController instance, and so trying to open the document never got past the NSDocumentController's "hmm, currently have that one open" check.
I did have an NSWindowController and NSDocumentController instance, but I've purged them from my project completely. I've overridden nearly every method for NSDocument trying to find out where the issue is. So far as I know, my Interface Builder bindings are all correct -- "Close" in the main menu is attached to performClose: of the First Responder, etc, and I've tried with fresh unsullied MainMenu and Document xibs as well.
I thought that it might be something strange with my bundle writing code, so I basically deleted it all and started from scratch, but that didn't seem to work. I took out my -init method overrides, and that didn't help either. I don't have the source of any simple document apps here, so I didn't try the next logical step (to substitute known-working code for mine in the readFromUrl and writeToUrl methods).
I've had this problem for about sixteen hours of uninterrupted troubleshooting now, and needless to say, I'm at the end of my rope. If I can't figure it out, I guess I'm going to try the project from scratch with a lot more code and intensity based around the bundle-document mess.
Hard to tell without code but I would suggest sending:
closeAllDocumentsWithDelegate:didCloseAllSelector:contextInfo:
... to the document controller and then looking at the controller as it is passed to the delegate to see how its state changes.
If the controller closes the document when you send the explicit message then your problem is with the binding to the window.

Resources