Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
HomeAnnouncementsWhite Papers
Discussion GroupsFirst AidDatabasesJavaBeansGUIJava 3DVirtual MachineCORBASecurityToolsGeneral
Java DirectoryOpen Source ProjectsSample Book ChaptersUser GroupsWeb Resources
Related Topics
Databases.NETMore Topics ...

Java Forum / GUI / October 2004

Tip: Looking for answers? Try searching our database.

Word-style undo / redo on a JTextPane

Thread view: 
Timo - 11 Oct 2004 14:17 GMT
Hello all,

I've written a small editor using a JTextPane without having any major
problems so far. My undo/redo implementation (using the
javax.swing.undo package) had one major disadvantage, though: The undo
command would only undo one single character at a time. Instead, it
would be nice to have an undo mechanism which, for example, records
all changes until an important event occurs, like the beginning of a
new paragraph. Then, it puts all these events together into one single
event. If the user does an undo, the undo would occur for the whole
paragraph instead of a single character - just like in Word. I know
about CompoundEdit to create such single edits, I also know about
using an UndoManager.

The big problem is, though, how to recognize these important events
such as paragraph beginnings etc. To look out for these events, I
created a class implementing UndoableEditListener which listens to the
edits. Each one of these edits contains specific information about the
edit event which can even be seen by printing the event to the
console, but I'm not able to access this information because it is in
a protected field - to be accurate, in the protected Vector edits
field of javax.swing.undo.CompoundEdit. Now my question is: Has anyone
done such an undo/redo implementation in Java and could point me into
the right direction? Am I already on the right track or do I have to
go deep down into Swing and rewrite half the internal event processing
stuff :) ?
Thanks a lot!
Timo
Thomas Fritsch - 11 Oct 2004 14:57 GMT
> Hello all,
>
[quoted text clipped - 24 lines]
> Thanks a lot!
> Timo
Did you already have a look at
http://www-uxsup.csx.cam.ac.uk/java/jdk-1.2.2/demo/jfc/Notepad/ or
file:///yourJDKdirectory/demo/jfc/Notepad/ ?
But I don't know whether the undo/redo implementation there deals with
things like paragraph beginnings. May you can see whether it does, when
playing with its menu item "Debug - Show elements".

Signature

Thomas<dot>Fritsch<squiggle>ops<dot>de

Timo - 12 Oct 2004 12:41 GMT
> Did you already have a look at
> http://www-uxsup.csx.cam.ac.uk/java/jdk-1.2.2/demo/jfc/Notepad/ or
> file:///yourJDKdirectory/demo/jfc/Notepad/ ?
> But I don't know whether the undo/redo implementation there deals with
> things like paragraph beginnings. May you can see whether it does, when
> playing with its menu item "Debug - Show elements".

Unfortunately, the Notepad example app has the same
one-character-at-a-time undo implementation. But the hint with the
Debug - Show elements may be very helpful: I could check the number of
Elements in the Document each time an edit occurs because inserting a
new paragraph creates a new Element in the Document. I'm certainly
gonna try this out.
Thanks a lot,

Timo
Thomas Weidenfeller - 11 Oct 2004 17:09 GMT
> (using the javax.swing.undo package)

Oh, that almost undocumented atrocity ...

> The big problem is, though, how to recognize these important events
> such as paragraph beginnings etc. To look out for these events, I
[quoted text clipped - 3 lines]
> console, but I'm not able to access this information because it is in
> a protected field

Something like this in the event handler:

public void undoableEditHappened(UndoableEditEvent ue) {
   if(ue instanceof AbstractDocument.DefaultDocumentEvent) {
      AbstractDocument.DefaultDocumentEvent de =
                            (AbstractDocument.DefaultDocumentEvent)ue;

      //
      // Apply whatever heuristics you need, to figure out to continue
      // UndoableEdit consolidation in a separate
      // CompoundEdit, to add the separate CompoundEdit, or to add a new
      // edit.
      //
      DocumentEvent.EventType type = de.getType();
      int len = de.getLenght();
           :
           .

   } else {
      //
      // Didn't get specific Document Event, continue to add the
      // otherwise unknown UndoableEdit
      //
      undo.addEdit(ue.getEdit());
   }
}

/Thomas
Timo - 12 Oct 2004 12:59 GMT
> > (using the javax.swing.undo package)
>
> Oh, that almost undocumented atrocity ...

Indeed :)

>        DocumentEvent.EventType type = de.getType();
>        int len = de.getLenght();

I thought about that and played a bit with the
AbstractDocument.DefaultdocumentEvent.getType() method, but this only
informs me about changes, inserts and removes on the document. It
propably won't handle caret moves and it looks pretty difficult to get
info about which key the user pressed and if he did some
cut/copy/paste stuff.
Thanks,

Timo
Alan Moore - 11 Oct 2004 22:33 GMT
> Hello all,
>
[quoted text clipped - 24 lines]
> Thanks a lot!
> Timo

Instead of having the listener evaluate the edits and decide what to
do with them, I find it's better to have the edits themselves make the
call.  The key is that, when the UndoManager receives an UndoableEdit,
it presents the new edit to the last edit it received and gives it a
chance to absorb the new one.  CompoundEdit only absorbs new edits if
isInProgress() returns true, but you can subclass it and override
addEdit() to change its criteria.  By wrapping all incoming edits in
this subclass, you can have certain edits chain themselves together
automatically, *after* they've been added to the UndoManager.

It still isn't simple, though.  While subclassing CompoundEdit gives
you access to the "edits" field, that doesn't do you much good.  The
actual edit information is still very difficult to get at, and there
isn't really enough of it.  It won't tell you, for instance, whether a
single-character insertion was the result of a keypress or a paste
operation, and a replacement will be seen by the listener as a
deletion followed by an (apparently unrelated) insertion.

What I do is wrap every user-initiated edit in a CompoundEdit subclass
that allows me to assign a unique presentation name to each kind of
operation.  I do this in the Action objects that actually perform the
operations, so there's never any doubt about which "atomic" edits are
part of what operation.  For example, in DefaultKeyTypedAction,
instead of:

   target.replaceSelection(content);

...I would have:

   target.beginCompoundEdit("typing");
   target.replaceSelection(content);
   target.endCompoundEdit();

...where "target" is now an instance of my custom JTextComponent.
Here are the actual methods:

 public void beginCompoundEdit(String name)
 {
   _compoundEditCount++;
   if (_compoundEdit == null)
   {
     _compoundEditNonEmpty = false;
     _compoundEdit = new NamedCompoundEdit(name);
     _compoundEdit.addEdit(
         new CaretEdit(getCaret().getMark(), getCaret().getDot()));
   }
 }

 public void endCompoundEdit()
 {
   if (_compoundEditCount > 0)
   {
     if (--_compoundEditCount == 0)
     {
       _compoundEdit.end();
       if (_compoundEditNonEmpty && _compoundEdit.canUndo())
       {
         _undoer.addEdit(_compoundEdit);
       }
       _compoundEdit = null;
     }
   }
 }

(As you can see, a compound edit may contain other compound edits, or
it may be completely empty--but I don't think either case ever really
happens.  I'll explain about the CaretEdit in a bit.)  While a
compound edit is in progress, it absorbs any atomic edits received by
the listener:

 public void undoableEditHappened(UndoableEditEvent evt)
 {
   if (_undoer != null && !_undoInProgress)
   {
     UndoableEdit edit = evt.getEdit();
     if (_compoundEdit != null)
     {
       _compoundEditNonEmpty = true;
       _compoundEdit.addEdit(edit);
     }
     else
     {
       // never happens
       _undoer.addEdit(edit);
     }
   }
 }

And at last, here's the NamedCompoundEdit class itself:

 /**
  * <p>NamedCompoundEdit allows us to assign a meaningful
  * presentation name to the edit by passing the name to the
  * constructor. Consecutive edits with the same name will
  * automatically be chained together so that they will all
  * be undone at once.</p>
  *
  * <p>Note that, for this to work properly, CaretEdits should be
  * added to the UndoManager whenever the user moves the caret (as
  * opposed to moves that are part of a compound edit or an
  * undo/redo). These insignificant edits will serve as separators
  * between significant edits where appropriate (e.g., if the user
  * types for a while, then moves the caret to another part of the
  * document and types some more, two separate UndoableEdits will be
  * created).</p>
  */
 class NamedCompoundEdit extends CompoundEdit
 {
   private String name;

   public NamedCompoundEdit(String name)
   {
     if (name == null || name.length() == 0)
     {
       throw new IllegalArgumentException("Invalid name: " + name);
     }
     this.name = name;
   }

   public String getPresentationName()
   {
     return name;
   }

   public String getUndoPresentationName()
   {
     return "Undo " + name;
   }

   public String getRedoPresentationName()
   {
     return "Redo " + name;
   }

   public boolean addEdit(UndoableEdit anEdit)
   {
     if (!isInProgress() && !canAbsorb(anEdit))
     {
       return false;
     }

     edits.addElement(anEdit);
     return true;
   }

   private boolean canAbsorb(UndoableEdit anEdit)
   {
     String editName = anEdit.getPresentationName();
     if (editName.equals(name))
     {
       return true;
     }
     else if ("typing".equals(name) &&
         ("Backspace".equals(editName) || "Delete".equals(editName)))
     {
       return true;
     }
     else if ("typing".equals(editName) &&
         ("Backspace".equals(name) || "Delete".equals(name)))
     {
       name = "typing";
       return true;
     }
     return false;
   }
 }

So, if two consecutive edits are received with the same name, the
first one absorbs the second.  I also treat "Delete" and "Backspace"
as equivalent to "typing" because, well, I do a lot of that, and it's
much less annoying this way :/

When an insignificant edit is added directly to the UndoManager, it's
just a placeholder, so CaretEdits serve to separate unrelated edits
without polluting the undo stack. When they're part of a CompoundEdit,
though, UndoManager actually pays attention to them; adding one to the
beginning of every compound edit causes the caret position and
selection to be restored as part or the undo.  I override the
fireCaretUpdate() method to generate the 'dummy' CaretEdits:

 protected void fireCaretUpdate(CaretEvent evt)
 {
   super.fireCaretUpdate(evt);
   _undoer.addEdit(
       new CaretEdit(getCaret().getMark(), getCaret().getDot()));
 }

And here's the CaretEdit class:

 class CaretEdit extends AbstractUndoableEdit
 {
   private int mark;
   private int dot;

   CaretEdit(int mark, int dot)
   {
     this.mark = mark;
     this.dot = dot;
   }

   public boolean isSignificant()
   {
     return false;
   }

   public String getPresentationName()
   {
     return "caret move";
   }

   public void undo() throws CannotUndoException
   {
     super.undo();
     try
     {
       setCaretPosition(mark);
       moveCaretPosition(dot);
     }
     catch (Exception ex)
     {
       throw new CannotUndoException(ex);
     }
   }

   public boolean addEdit(UndoableEdit edit)
   {
     if (edit instanceof CaretEdit)
     {
       edit.die();
       return true;
     }
     else
     {
       return false;
     }
   }
 }

There's a problem with the dummy edits, though.  If I undo an edit,
then move the caret, I can't redo the edit that I just undo'd (dude!).
UndoManager doesn't care that CaretEdits are not significant; it just
throws away the whole redo stack.  I'm going to have to come up with
another way to handle that.
Timo - 12 Oct 2004 17:01 GMT
<snip>

> What I do is wrap every user-initiated edit in a CompoundEdit subclass
> that allows me to assign a unique presentation name to each kind of
> operation.  I do this in the Action objects that actually perform the
> operations, so there's never any doubt about which "atomic" edits are
> part of what operation.

Wow. Seems that you already spent a lot of time on this problem :) It
looks a bit complicated, but very very promising. Fortunately, the
weather is bad and I got nothing else to do for the next few days so
there'll be plenty of time to try it out :) Thank you very much for
this!

Timo
Alan Moore - 12 Oct 2004 19:37 GMT
> Wow. Seems that you already spent a lot of time on this problem :) It
> looks a bit complicated, but very very promising. Fortunately, the
[quoted text clipped - 3 lines]
>
> Timo

Yeah, I've spent a lot of time on this problem, but this is one
feature that's just not negotiable--if you're going to have Undo at
all, it has to work at least this well.  And if you're not going to
have Undo, why are you bothering to write an editor? ;)

There may be a simpler way to get undo to work sensibly WRT typing,
but my approach has another big advantage: the presentation name
always relfects what the user actually did.  In the default
implementation, the UndoableEdits have totally generic names like
"addition", "removal", "change" (or something like that).  But in my
editor, when I hover the mouse pointer over the Undo button, the
tooltip says, e.g., "Undo typing" or "Undo Block Comment".

I should mention that the approach I use was originally copied from
jEdit.  My version has changed considerably since then, and I believe
jEdit's has, too.  So that's another resource you could could check
out.

Alan
Timo - 25 Oct 2004 21:47 GMT
Time flies! After wrecking my car (and someone else's - thank heavens
no one was hurt) and beginning a new year at the university I finally
found a little time to look into this problem again. And after quite a
few failed attempts of doing this on my own, I have another question.
Alan wrote:

> For example, in DefaultKeyTypedAction,
> instead of:
[quoted text clipped - 6 lines]
>     target.replaceSelection(content);
>     target.endCompoundEdit();

I guess by DefaultKeyTypedAction, you mean
DefaultEditorKit.DefaultKeyTypedAction. So I tried to replace this
action with one of my own, but with no success. I tried a lot of
subclassing DefaultEditorKit, but none of it worked and there seems no
method such as DefaultEditorKit.setAction() to replace an action. Am I
on the right track here but missing a few points or am I entirely on
the wrong way?
Thank you for any help!

Timo
Alan Moore - 26 Oct 2004 08:47 GMT
> I guess by DefaultKeyTypedAction, you mean
> DefaultEditorKit.DefaultKeyTypedAction. So I tried to replace this
[quoted text clipped - 3 lines]
> on the right track here but missing a few points or am I entirely on
> the wrong way?

I didn't go into how to replace a text component's actions with your
own because there was already so much information in my previous post,
but it isn't particularly obvious.  With most actions, you can get the
text component's ActionMap and put your own action in it under the
same key to replace the original one.  But the default ("typing")
action is a special case; the only way to get at it is through the
Keymap.  Here's some sample code that shows how to replace both kinds
of action, along with a way to tack on the undo-related stuff.

===================================================================

import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.text.*;

public class Test
{
 public static void main(String[] args)
 {
   JTextField tf = new JTextField();

   Action bksp = tf.getActionMap().get(
       DefaultEditorKit.deletePrevCharAction);
   tf.getActionMap().put(
       DefaultEditorKit.deletePrevCharAction,
       new InstrumentedAction(bksp, "Backspace"));

   Action def = tf.getKeymap().getDefaultAction();
   tf.getKeymap().setDefaultAction(
       new InstrumentedAction(def, "typing"));

   JOptionPane.showConfirmDialog(null, tf, "Test",
       JOptionPane.OK_CANCEL_OPTION,
       JOptionPane.PLAIN_MESSAGE);
 }

 static class InstrumentedAction extends AbstractAction
 {
   private Action delegate;
   private String label;

   public InstrumentedAction(Action delegate, String label)
   {
     this.delegate = delegate;
     this.label = label;
   }

   public void actionPerformed(ActionEvent evt)
   {
     System.out.println("Performing action: " + label);
     // beginCompoundEdit(label);
     delegate.actionPerformed(evt);
     // endCompoundEdit();
   }
 }
}

=============================================================

I just noticed that the default action gets executed whenever, e.g.,
backspace or Ctrl-V is pressed, in addition to the correct actions for
those keys.  Obviously it doesn't hurt anything, though, or I would
have noticed it long before this!

There's a boatload of actions in DefaultEditorKit, but you only have
to replace the ones that mutate the document:

 DefaultEditorKit.insertBreakAction
 DefaultEditorKit.insertTabAction
 DefaultEditorKit.deletePrevCharAction
 DefaultEditorKit.deleteNextCharAction
 DefaultEditorKit.cutAction
 DefaultEditorKit.pasteAction
 DefaultEditorKit.insertContentAction

(Actually, that last one doesn't seem to be used for anything.)


Free Magazines

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

Oracle MagazineNetwork ComputingComputer WorldBio-IT WorldeWeekInformation WeekInfosecurity
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2008 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.