Java Forum / GUI / March 2005
UndoManager addEdit problem
jonck@vanderkogel.net - 03 Mar 2005 17:36 GMT Hi, I'm using the UndoManager to undo/redo the input of a user to a form with multiple JTextFields. The standard UndoableEdit's produced by the JTextFields are one character at a time however and I would like to change this behavior so that multiple characters can be undone/redone with a single undo/redo action.
So I wrote a class that extends AbstractUndoableEdit, and overrides the addEdit method. When a user has just entered a new JTextField (determined by a FocusListener) the first edit will be marked as that it should add edits, all subsequent edits in this JTextField will thus be merged into one edit.
This works great, however, when the user then switches to a different JTextField the new UndoableEdits are still being absorbed by the first edit. My question therefore is, how do I notify to the UndoManager (or the last UndoableEdit in the UndoManager's queue) that I would like the last edit to stop absorbing any new edits and start a new edit?
Thanks for any help, Jonck
John McGrath - 04 Mar 2005 02:41 GMT > So I wrote a class that extends AbstractUndoableEdit, and overrides the > addEdit method. When a user has just entered a new JTextField [quoted text clipped - 7 lines] > the last UndoableEdit in the UndoManager's queue) that I would like the > last edit to stop absorbing any new edits and start a new edit? You are generating your own UndoableEdits? I presume that they must know the Document that they apply to, since they need to know that in order to undo/redo the edit.
In that case, all you should need to do is, in addEdit(), check whether the Document for the new edit is the same as the Document for the existing edit. If not, do not absorb the edit.
 Signature Regards,
John McGrath
jonck@vanderkogel.net - 04 Mar 2005 11:01 GMT Right, but the problem is that once you state that an edit should absorb the next by returning true in its addEdit method, all edits that are from then on added to the UndoManager will be absorbed (even if I let the next edits return false in their addEdit method). So how do I tell it not to absorb any more edits?
Thanks, Jonck
John McGrath - 04 Mar 2005 13:57 GMT > Right, but the problem is that once you state that an edit should > absorb the next by returning true in its addEdit method, all edits that > are from then on added to the UndoManager will be absorbed (even if I > let the next edits return false in their addEdit method). That is not how it should work. When you add the edit to the UndoManager, it will try adding the edit to the most recent edit. If that addEdit() call returns true, then the UndoManager just returns - the edit has been added. If it returns false, it will add the edit to the end of its list of edits. (Actually, there is also the replaceEdit() processing, but I assume you have not overridden that).
How have you implemented your addEdit() method? Typically, it would just merge the new edit into itself, if possible. If the last edit did an [insert "a"] and you add an edit to it that does an [insert "b"], then addEdit() on the last edit would change the string that it inserts from "a" to "ab" and return true. However, if you tried adding a different type of edit, say a [remove "c"], it would return false. Then the UndoManager would just add the "remove" edit to its list.
How are you getting the UndoableEdits to your UndoManager? Did you just add the UndoManager as a listener, or did you create your own listener that adds the edits? If the latter, could you post that code?
 Signature Regards,
John McGrath
jonck@vanderkogel.net - 12 Mar 2005 11:23 GMT Sorry for the late reply, I was out with the flu all week.
Here is the code for my addEdit method:
public boolean addEdit(UndoableEdit anEdit) { if (shouldAdd()) { UndoableGpsJTextFieldEdit edit = (UndoableGpsJTextFieldEdit) anEdit; edit.setAfter(getAfter()); } return shouldAdd(); }
The shouldAdd() method returns whether or not this particalur edit should be added to the previous one or not (set at constructor time). The setAfter() and getAfter() methods store and retrieve the value of the TextField after the edit took place. So in other words, here's what I intended to happen: the first edit of a TextField I set add to false, so a new edit should be put in the UndoManager. Subsequent edits I then tell to be added. The addEdit method sets the first edit's after value to the new value of the TextField. When a TextField loses focus I reset the component, so that should it regain focus a new edit will be created, etc...
But this is not at all how things are working. A new edit is never created, all edits are being absorbed by the first one (while I am 100% sure that when the user switches to a new TextField, the add property is set to false).
Regards, Jonck P.S. I have simply set the UndoManager as a listener for new edits, so therefore I did not post any of that code.
John McGrath - 12 Mar 2005 22:12 GMT > Here is the code for my addEdit method: > [quoted text clipped - 6 lines] > return shouldAdd(); > } I have a few questions about this code.
> edit.setAfter(getAfter()); Shouldn't this be the following?
setAfter( edit.getAfter() );
The edit for which the addEdit() method is executing is the previous edit, the one that is already in the UndoManager queue. It is the "this" edit that is supposed to absorb anEdit, not the other way around, so the "this" value should be modified.
I also wonder about the way that you call shouldAdd() twice. First, it is not the most efficient way to do things. Why compute the value twice? But far more important than that is the question of whether it will return the same value from both calls. Even if you know that it will, it is better if this code is written so that the inconsistency cannot happen.
Finally, the shouldAdd() method cannot see the "anEdit" parameter that is passed to addEdit(), so I wonder, how is it supposed to determine whether the edit can be added? It looks like you will get a ClassCastException if an edit other than an UndoableGpsJTextFieldEdit were passed to addEdit().
For comparison, here is the addEdit() method from an UndoableEdit implementation that I wrote a while back:
public boolean addEdit( UndoableEdit edit ) { if ( edit instanceof MoveEdit ) { MoveEdit e = (MoveEdit) edit; if ( target == e.target ) { newPosition = e.newPosition; return true; } } return false; }
It occurs to me now that I probably should have added an e.die() just before the return true. I do not think it is critical, but it would be appropriate.
> P.S. I have simply set the UndoManager as a listener for new edits, so > therefore I did not post any of that code. That works, but I usually want to update the state of the undo and redo Actions, so that the user knows whether undo and/or redo can be performed. So I use a listener something like this:
private UndoableEditListener editListener = new UndoableEditListener() { public void undoableEditHappened( UndoableEditEvent event ) { undoManager.addEdit( event.getEdit() ); updateUndoActions(); } };
 Signature Regards,
John McGrath
jonck@vanderkogel.net - 13 Mar 2005 12:16 GMT > The edit for which the addEdit() method is executing is the previous edit, > the one that is already in the UndoManager queue. It is the "this" edit > that is supposed to absorb anEdit, not the other way around, so the "this" > value should be modified. Aha, that explains it! Yes, it works as I would expect it now. Thank you!
> Finally, the shouldAdd() method cannot see the "anEdit" parameter that is > passed to addEdit(), so I wonder, how is it supposed to determine whether > the edit can be added? The shouldAdd() method is just the getter for a boolean field, which I set at construction time. I guess it would have been better if I would have adhered to the standard, and had named this method getAdd() or isAdd() (in this particular case shouldAdd() seemed appropriate, but taken out of it's context I can understand how it becomes confusing). Whether or not the edit should be added is determined in the JTextField (a subclass of JTextField to be more precise). It contains a FocusListener, and by keeping track of when a component gains or loses focus it determines whether an edit is the first edit (and thus should not be added) or whether an edit is a follow-up (in which case I want the edit to be added to the last).
> It looks like you will get a ClassCastException if > an edit other than an UndoableGpsJTextFieldEdit were passed to addEdit(). Yes, to simplify my posting I took out all "non-relevant" code like type checking, error handling, etc...
> That works, but I usually want to update the state of the undo and redo > Actions, so that the user knows whether undo and/or redo can be performed. I have extended the UndoManager so that listeners can be added to it, and it passes the edit on to any listeners attached to it. So in my GUI code I have attached a listener to the UndoManager which updates the state of the undo/redo buttons. So in other words, the UndoManager listens to the components on the form and the GUI listens to the UndoManager.
Thank you very much for your excellent help, I am glad we got to the bottom of this!
Regards, Jonck
John McGrath - 14 Mar 2005 02:43 GMT > Aha, that explains it! Yes, it works as I would expect it now. Thank > you! Great!
> The shouldAdd() method is just the getter for a boolean field, > which I set at construction time. I have some doubts about this. Deciding when you create the edit may work, given what you know about the global environment in which they are being created, but that environment may change, breaking this code.
I would think that the more appropriate way to decide whether an edit should be absorbed would be based on the current edit and the new edit. The edit should know what field it applies to, so it could just compare the two fields and decide not to absorb the edit if the fields do not match.
> I have extended the UndoManager so that listeners can be added to it, > and it passes the edit on to any listeners attached to it. That sounds very reasonable. I have often wondered why the standard UndoManager does not already have this feature. Adding a listener would make a lot more sense than creating an intermediary to post the edits.
 Signature Regards,
John McGrath
jonck@vanderkogel.net - 14 Mar 2005 14:21 GMT > I would think that the more appropriate way to decide whether an edit
> should be absorbed would be based on the current edit and the new edit. > The edit should know what field it applies to, so it could just compare > the two fields and decide not to absorb the edit if the fields do not
> match. Very good suggestion, this does indeed sound like a much better design. Thanks!
Alan Moore - 04 Mar 2005 11:10 GMT >Hi, >I'm using the UndoManager to undo/redo the input of a user to a form [quoted text clipped - 16 lines] > >Thanks for any help, Jonck I suppose the simplest solution would be to assign a separate UndoManager to each textfield. But it sounds like UndoManager is overkill for this problem anyway. It would prpbably be more appropriate to create an action that restores the textfield to the state it was in when the form was displayed, and bind the action to (e.g.) the ESC key.
jonck@vanderkogel.net - 04 Mar 2005 12:58 GMT The UndoManager is definitely not overkill, since I not only have JTextFields on my form, but also JComboBoxes, JPanels with pictures, etc... and I want the user to be able to have a long chain of undos/redos available to him should he need them.
I have solved the problem btw, by turning things around. Instead of telling the first edit of a textfield to absorb the next edits, I tell the first edit that it should not replace the last edit and all following edits of the same textfield will replace this edit. If the user switches to a next textfield, a new edit that does not replace the last one is sent to the UndoManager etc... In this way there is one edit per textfield (per time the user focuses on that textfield) that is able to restore the state to the last value it was at.
But anyway, I do find the current discussion interesting and I would like to find an answer to it nonetheless, purely out of curiosity.
<back to the old problem>
If I do assign a seperate UndoManager to each textfield (leaving the other components out of the formula for now), keeping track of which undo/redo action is next in line would be quite a chore (imagine the following situation: user makes a change to field1 --> edit1 of UndoManager1, then he edits field2 --> edit2 of UndoManager2, then goes back to field1 and does another edit --> edit2 of UndoManager1). Doable, but it seems like a very inelegant solution imho.
Regards, Jonck
John McGrath - 05 Mar 2005 05:35 GMT > But anyway, I do find the current discussion interesting and I would > like to find an answer to it nonetheless, purely out of curiosity. Based on your description of the code with the addEdit(), it should have worked. Without a runnable example of the problem, it is hard to tell what is wrong.
 Signature Regards,
John McGrath
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|