Time to figure out how to make a certain textfield gain focus. Remember, when doubleclicking the countries table, we want the drawer to slide open and the first field in that drawer to get keyboard focus automatically. We already got the drawer to open, so now is the time to get that focus thing working.
It turns out that in Cocoa, each window maintains a pointer to the view that has keyboard focus. This is so, even if that view is part of another view, so the pointer can go “past” several levels of views. Views don’t have any such pointer.
Checking out our app, it turns out that the “Countries” pane with the browser table in it is an NSPanel, which is an NSWindow and it is controlled by our CountriesController object, which is derived from an NSWindowsController, that in turn contains a pointer to our window. The drawer itself (DetailsView) is an NSView, so it does not contain a pointer to the field with focus (or “first responder” as the cocoa docs define it).
The docs for NSWindow gives us the makeFirstResponder method. The docs for NSWindowController gives us the “window” method. Together, this let’s me guess that a good way of making an arbitrary field a first responder is with the following call:
The assumption here is that we call it from inside the CountriesController, that’s why it’s a call to “self”, and that the parameter is an object derived from NSResponder, which all views are (I think).
The next problem is how to get the pointer to that “arbitrary field”, and let’s use outlets for that once again. They’re neat.
Add another outlet to the CountriesController.h header file:
Save the header file, drag and drop it on the countries.nib pane in Interface Builder (no, I’m not going to show it yet again, there are several screen shots of this already in this series). Click “merge” as I do, unless you have a reason not to.
Now, control-drag from the “File’s Owner” (which is a reference to our CountriesController) to the actual field you want:
In the inspector pane, doubleclick the “firstDrawerField” line to connect it:
Finally, add a line to the implementation in the CountriesController.m file:
Save, build, run, and be amazed. It works. When you doubleclick a country in the table, the drawer opens and the first field gets the focus. Yay!
Now it’s time to add save and cancel buttons to the drawer. Open the countries.nib file in Interface Builder and add two neat buttons to the DetailsView pane:
These buttons need to trigger actions in the CountriesController, so we add methods for that in the CountriesController.h file:
We now do the familiar drag-and-drop of that header file from XCode to the Countries.nib pane in Interface Builder. After doing that, we can control-drag from the buttons we added to the actions we just created:
Set the action to the “detailsViewCancelButton” by doubleclicking that row in the inpector or by clicking the “Connect” button:
Repeat that process for the “Save” button and the “detailsViewSaveButton” action.
Now we come to a more difficult part. Naturally, I want the “Cancel” button to close the drawer without actually applying any changes to the entity I’m editing, in this case “country”. Core Data is built in such a way that it directly updates the entity in memory, which we can see when we edit a field in the drawer and the corresponding column in the browser table changes immediatly we tab away from the field in the drawer. This doesn’t yet change the contents in the database, however.
I see three ways of handling this:
- Save the entity to the database before the edit, then either save or rollback the changes depending on which button the user clicks.
- Maintain the variables before and after changes in code in the entity managed object, then copy the old values back if the user clicks “cancel”
- Use the built-in undo manager
None of the above alternatives are perfect, and this is the reason why:
Save and rollback
If we have nested edits, this will fail. By a “nested edit” I mean, for example, that you create an invoice, and while doing so you create a new customer. If you then “save” after creating the customer, you’ll at the same time save the invoice that is in preparation and incomplete. Or any variations on that theme. Not good.
Maintain the variables in code
This ought to be the most complete handling of the problem, but at the same time it circumvents the built-in functionality in core data and uses too much code. I don’t like it.
Use the built-in undo manager
This is my preferred solution. But it isn’t without problems. For instance, if I do a nested undo, with one group being the invoice and one being the customer, what happens if I save the customer, but undo the entire invoice? Actually, I don’t know. I’d like the customer to not be undone, but one can argue that it should be. So, we’ll just wait and see what these more complex sequences will bring.
It seems that the undo manager that is part of the object manager context is intricately used inside the managed objects. I added a boatload of tracing to the methods to find a way to make it work right, and the following seems to cover it, at least experimentally. At the start of the edit, you need to:
- make the first edit field the first responder
- make the object context process any pending changes (no, this does not mean saving to disk)
- start a new undo grouping
- disable “group by event” in the undo manager
Note that the order of these (and the following) operations is critical.
If you save changes, do the following:
- make the clicked button the first responder (this is just a trick to complete the last edited field, if the user didn’t tab out of it before he clicked the button)
- tell the managed object context to process pending changes
- tell the undo manager to end the undo grouping
- tell the undo manager to again group by event
Note that you don’t save to disk in this operation.
If you instead cancelled the edits, this is what has to be done:
- make the clicked button the first responder
- tell the managed object context to process any pending changes
- tell the undo manager to end the grouping
- tell the undo manager to undo the last nested group
- tell the undo manager to enable groups by event again
Let’s look at some code
I added a litte function to trace what happens during my experiments with the undo manager:
Then I called this function like crazy during the methods, like in the following example:
I’m not going to bore you with the instrumented version of the methods, so this is how everything looks after removing the tracing code:
When a row is doubleclicked in the browser, this method is called:
Then, when you click “save”, this is what is called:
Finally, when you click “cancel” this method gets called:
Experimenting with the app shows that the save and undo seems to work as intended, with the glaring exception of the radiobuttons. If I cancel, they don’t revert. Owell, that has to be the topic of another chapter in this saga.