Stay Responsive
November 30th, 2006In general, a user’s changes to a Cocoa NSTextField are saved when they finish editing (by tabbing or clicking to another field) or press return. This is fine, and it works 95% of the time. But on occasion we may find good reason to saved a user’s changes regardless of whether they’ve performed one of these completing actions.
For example, in the “Send to iTunes” feature I’m working on for FlexTime, I decided it was quite possible that a user would fine-tune an activity’s details, say change the text of a “Speak Text” cue, and then immediately select “Send to iTunes.” What should happen in this case? Certainly the user’s changes should be committed and included in the exported audio. The user hasn’t “finished editing” in a conventional way, but by choosing to export the routine, they’ve let me know they are expecting to hear what they’ve just typed.
The cleanest way to end editing for a particular window was explained to me a few years ago by Erik Buck, who kindly explained on a mailing list that it’s best to first ask the window to become firstResponder (thus taking firstReponder away from the focused field), and if that failed, to resort to the crude “endEditingFor:” method on NSWindow.
This works! But it has the side-effect of causing there to no longer be a reasonable first responder for the window. In the scenario I’m dealing with, I want to:
- Commit Edits.
- Send to iTunes.
- Put things back how they were.
I figured if I could be responsible for taking first responder status away from some field in my window, I could just give it back after I was done. But things don’t work out so easily in a world where the mysterious field editor is always being injected into the view hierarchy. My first naive solution resulted in my essentially saving and restoring the field editor as the first responder for the window. But after the editing has ended, the field editor isn’t even on the screen anymore. So setting it to first responder just causes a weird focus-free situation that you can’t even tab out of.
The solution, I realized, was to detect the field editor being the first responder and, in that situation, treat its delegate as the responder that needs to be restored after editing. Now, I’m pretty sure that the delegate of a field editor will always be an NSResponder, but just to be sure, I do some runtime checking. The resulting code for “commit edits while saving and restoring the first responder” looks something like this:
// Save the current first responder, respecting the fact // that it might conceptually be the delegate of the // field editor that is "first responder." id oldFirstResponder = [oMainDocumentWindow firstResponder]; if ((oldFirstResponder != nil) && [oldFirstResponder isKindOfClass:[NSTextView class]] && [(NSTextView*)oldFirstResponder isFieldEditor]) { // A field editor's delegate is the view we're editing oldFirstResponder = [oldFirstResponder delegate]; if ([oldFirstResponder isKindOfClass:[NSResponder class]] == NO) { // Eh ... we'd better back off if // this thing isn't a responder at all oldFirstResponder = nil; } } // Gracefully end all editing in our window (from Erik Buck). // This will cause the user's changes to be committed. if([oMainDocumentWindow makeFirstResponder:oMainDocumentWindow]) { // All editing is now ended and delegate messages sent etc. } else { // For some reason the text object being edited will // not resign first responder status so force an /// end to editing anyway [oMainDocumentWindow endEditingFor:nil]; } // If we had a first responder before, restore it if (oldFirstResponder != nil) { [oMainDocumentWindow makeFirstResponder:oldFirstResponder]; }
This works perfectly for my needs and hopefully it will at least put you on the right path towards making your app work perfectly, too.
November 30th, 2006 at 7:40 pm
I’ve done something similar before, so much in fact that I’ve added a method on NSWindow in a category. Although I’m not checking (currently) that the delegate is an NSResponder subclass, this works for me.
– (void)endEditingSavingCurrentResponder
{
NSResponder *currentResponder = [self firstResponder];
if ([currentResponder isKindOfClass:[NSTextView class]]) {
currentResponder = [(NSTextView *)currentResponder delegate];
}
if (![self makeFirstResponder:self])
[self endEditingFor:nil];
[self makeFirstResponder:currentResponder];
}
November 30th, 2006 at 7:46 pm
Hi Ashley! I think your basic plan is right, but I wonder if it might fail if there is actually an NSTextView (not a field editor) in your window, that has focus at the time. In that case the delegate (likely to be like a controller class in your app) will be grabbed and attempted to be made the responder.
November 30th, 2006 at 9:18 pm
Another possibility that may apply is to bind the text field’s value and check the “continuously updates value” option.
November 30th, 2006 at 9:34 pm
That’s a good point, I’ve not used that code with an NSTextView in my nib files before. I’ve modified my code now, proactively reducing any future bugs relating to that.
November 30th, 2006 at 10:13 pm
ken: whoah, I hadn’t considered that the “continuously updates value” option could be applied to a text field. But in this case I think it would be overkill because it’s only in the very specific situation that a user chooses to export that I want to “immediately update.”
December 1st, 2006 at 3:16 am
I’m with ken on this… immediate updating is the way to go.
December 1st, 2006 at 7:18 am
ssp: I can see that as a sort of ideal user experience – sometimes. Though frankly I think even the ideal behavior of, for instance, a text title simultaneously updating a window’s title as the user types would be so contrary to the ordinary behavior that some users might annoyed or distracted by it.
One problem that comes to mind is that by sending continuously you’d be exercising the formatter and/or validation method for the field, probably before the user has finished typing valid information. This has the potential to cause text fields to behave like those infuriating telephone-entry fields, where you never know whether the computer or the user is responsible for tabbing to the next field.
In general, I’d be wary of violating the traditional “safe, ponderous editing” phase that users now enjoy in most text fields.
December 1st, 2006 at 7:39 am
“continuously updates value” has its uses, but it isn’t always the right solution. One time where you don’t want to do this is when setting the value on the model is undoable.
Imagine typing “Fred” for someone’s name, then committing the edit in the field.
Undo should put it back to its previous value, without requiring you to step through
“Fre”
“Fr”
“F”
“Previous Value”
December 1st, 2006 at 10:29 am
Personally, I’ve always found NSObjectController and -commitEditing to work nicely.
December 1st, 2006 at 1:00 pm
Mike,
-commitEditing is a the correct way to end editing for views bound to a controller. However, this has the same problem Daniel sought to avoid – after you are finished the window’s first responder is no longer the control the user left keyboard focus in. (And it requires the same sort of treatment.)
December 2nd, 2006 at 6:01 am
Ah, I hadn’t realised that -commitEditing messed with the first responder. I guess I think I must of only ever used it in situations where a sheet is about to appear, so the first responder doesn’t matter.
December 2nd, 2006 at 9:21 am
Mike: but when the sheet is then dismissed, your user would probably prefer to have the original focused field remain focused. That’s exactly the situation I’m contending with here.
December 3rd, 2006 at 8:08 am
A very good point. I shall have to look into this :)
December 6th, 2006 at 5:05 am
Daniel, thank you for the tip! This works like a charm. And for what its worth, this very situation where a user clicks the plus button before “officially” ending their editing task seems to have popped up with a decent number of Actiontasic users. I imagine that some of them get in a routine of typing while leaving the mouse over the plus button for a quick click when entering a series of items.