Make one last change to NSDocument before app terminates/window closes? - cocoa

I have a basic NSDocument-based app. I need to make one last change to the document when:
The user closes the document's window
The user terminates the app
Why?
The document window contains an NSTextField. Usually text entered into this text field is committed to the document's model after the user presses Enter (via textDidEndEditing(_:)).
Let's assume the user typed some text, but does not press Enter. Instead he presses Cmd-W or Cmd-Q to close the document window or terminate the app altogether.
textDidEndEditing is not called so I check if the text field contains changes and try to update the document myself.
❌ Here is where it gets tricky. The following results in a deadlock on NSDocument.performActivityWithSynchonousWaiting:
override func viewWillDisappear() {
super.viewWillDisappear()
undoManager?.disableUndoRegistration()
textField.commitEditing() // Updates the model
document.updateChangeCount(.changeDone)
}
I managed to work around the deadlock by not hooking into viewWillDisappear, but into NSDocument.canClose(withDelegate delegate: Any, shouldClose shouldCloseSelector: Selector?, contextInfo: UnsafeMutableRawPointer?).
✅ This code causes the changes to be saved when the user closes the document window:
override func canClose(withDelegate delegate: Any, shouldClose shouldCloseSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) {
undoManager?.disableUndoRegistration()
textField.commitEditing() // Updates the model
updateChangeCount(.changeDone)
super.canClose(withDelegate: delegate, shouldClose: shouldCloseSelector, contextInfo: contextInfo)
}
.
❓Unfortunately, the above is not called when the user terminates the app. I tried updating my document in applicationWillTerminate() -- did not work. I also tried overriding applicationShouldTerminate() and delaying termination for 3 seconds. I can see the document's window being marked as "edited", but changes are not saved.
How can I make a last change to NSDocument right before the app terminates and have it saved automatically?

For reference: I don't think it is possible to make a last change to NSDocument while the app is being terminated. The document architecture is simply not designed that way (see https://www.mail-archive.com/cocoa-dev#lists.apple.com/msg107739.html)
At the time the app is terminating, the document's windows have been closed and window controllers released.
I eventually solved this problem by changing the design of my app to record changes to the document as the user types. I use a new instance of UndoManager and manually call NSDocument.updateChangeCount(.changeDone) to register my changes out of the undo/redo chain. When the user finally commits changes by pressing Enter, I use the document's undo manager to register the change.

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

macOS App: Don't Let Sync Changes Overwrite User Typing

Xcode 10.1, Swift 4.2, Realm 3.12 (database)
I have a Mac app where multiple users collaborate and the data syncs across multiple devices. So data is always changing.
Throughout the app, I write code that updates the UI when a sync occurs in the background. A problem I keep running into is that when a user is typing in an NSTextField, a sync will happen (from another user's changes) and the NSTextField where they are typing will suddenly revert to the new sync value, and the user will lose what they typed.
Here is a demo Mac app I put together that simulates the problem. If you start typing in the field, every 10 seconds the field updates: https://d.pr/f/8iXjqx
I'm wondering what strategies other Mac devs have used to avoid these collisions.
Is there a way to tell an NSTextField to not update if there is a cursor present in it? Do I have to save the value to my local database with every keystroke and then somehow do a merge when a sync happens?
In your textFields I would catch whether or not you are active:
override func becomeFirstResponder() -> Bool
{
isFirstResponder = true
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool
{
isFirstResponder = false
return super.resignFirstResponder()
}
Then when you go to update the text, leave it if that flag is set.
Assuming you don't want to merge the text in some way, refresh the text to the model value if no edits have been made when you resign, otherwise write the change back to the model.

NSDocument with multiple windows -- Appkit dialog sheets jump to the wrong window

It is rarely used, but Apple's NSDocument documentation describes how to set up an NSDocument with multiple windows for a single document. I'm working on a database application that does this. Here is an example of a checkbook database document with two windows open. Each window shows a different view of the document, in this case a spreadsheet like view in the back, and a chart summarizing this data set in the front window. This example shows two windows for one document, but the user can create as many windows per document as they want, each displaying the same underlying document in a different way.
Everything works fine, except that if a system dialog sheet is opened (Save As, Print, Page Setup), most of the time (but not every time) the dialog sheet jumps to another window and attaches to that window instead of the current window, as shown in this movie.
Notice that although the dialog sheet attaches to the window containing the chart, it is correctly printing the content in the spreadsheet window. If I press Print, the correct content will be printed.
For printing, all our code does is call the NSDocument printDocument: method.
[NSApplication sendActionToFirstResponder:#selector(printDocument:)];
Page Setup code is also just calling NSDocument.
[NSApplication sendActionToFirstResponder:#selector(runPageLayout:)];
Our code is not customizing any of these dialog sheets, they are completely stock.
For the Save As command there is no code in our application at all, this appears automatically in the menu when the option key is pressed.
This problem appears in all versions of macOS supported by our application, from 10.9 thru 10.13. Perhaps this is an AppKit bug that is rarely seen because multiple windows with a single document is so rarely used?
This problem doesn't cause a crash, or prevent a user from doing what they want, but it is very visibly incorrect and reduces user confidence in the quality of the program.
For my reference this is #221 in the Panorama X issue tracker.
Implement/override NSDocument property windowForSheet.
The value of this property may be nil, in which case the sender should present an app-modal panel. The NSDocument implementation of this property sets the value to the window of the first window controller, or [NSApp mainWindow] if there are no window controllers or if the first window controller has no window.

Undo Manager is nil --- Why?

Why is [self undoManager] zero in a child window, in a doc-based app?
Should it not refer to the undo manager of its parent window? In the parent window, I get an actual address for the undo manager!
The undoManageris not a member of NSWindowController.
This is just a NSDocument "feature".
An excerpt from the NSDocument docs :
... A document manages its window’s edited status and is set up to perform undo and redo operations. ....
Section "Subclassing NSDocument":
.... Subclasses are also responsible for the creation of the window controllers that manage document windows and for the implementation of undo and redo. ....
The code you've written won't work on other strongly typed languages because you would send a message to an object that doesn't exist. I'm pretty sure you should have a compiler warning here.
Hope this helps,
best,
Flo
Flo's answer was a good starting point. Some time later, it turns out that the responder chain is somehow acting up (or, it may be me :-) ).
The child window, controlled by NSWindowController, should automatically (??) have a document property so that [self document] returns the document associated with this window. It's easy to pull the Undo manager from that.
However, in my application (and in a small testing app, too) this document is not set. When I set it manually from within the document ([newWindow setDocument:self]), everything works: registering the undo/redo actions, the menu bar, etc.

Applescript and Microsoft Word

I'm working on a applescript to update the content of a document in Microsoft Word. The updating process is quite long (might take more than 5s). So I want to prevent users to change anything during the updating. Do you know whether Microsoft or Applescript a function like that?
In Windows, I can just display a User Form (which is a dialog telling that "we are updating... ") and close that form when it's done. However, I don't know whether I can do the same in Mac (with Applescript alone).
When you say "applescript", I don't know if you mean "plain" applescript or the AppleScriptObjC version. If you mean the latter, then I know ways to do it.
One way I've used during slow processes is to put an overlay view over the whole content view of the window. I make it translucent white to partially obscure the window, and put some kind of message (and maybe a progress indicator) on it. You can just use an NSBox (of the custom type) in IB to make this, and then make a subclass of NSBox to color the view and override mouseDown:. MouseDown:, doesn't need to have any code in it, just by overriding it, you capture any key and mouse events so they don't accumulate on the event queue, and get used by the view below after your overlay goes away. Here's code I've used:
script Overlay
property parent : class "NSBox"
on awakeFromNib()
set overlayColor to current application's NSColor's colorWithCalibratedWhite_alpha_(1,.8)
setFillColor_(overlayColor)
end
on mouseDown_(theEvent)
--log "mouseDown"
end
end script
I have this view as the top most view in the view hierarchy, and set its hidden property to true until I want to show it.

Resources