Java Tips Weblog

  • Blog Stats

    • 1,283,642 hits
  • Categories

  • Archives

Compound Undo Manager

Posted by Rob Camick on October 27, 2008

Swing text components support undo/redo of text entered in a Document. Every time a change is made to the Document an UndoableEditEvent is generated. The UndoManager keeps track of these events and allows you to invoke undo/redo on them. The default implemetation of the UndoManager is to undo/redo individual events. This means each character you type is undone/redone separately. I don’t know about you, but I would rather undo/redo releated events together at the same time.

The first thing we need to do is to define what related events are. Characters typed sequentially into a Document or removed sequentially from a Document should be related. That is, if you type a few characters, use the backspace key to correct a typo and then continue typing, the events should be related. Also, any attribute changes created as a result of the characters entered should be related. This would happen, for example, when doing syntax highlighting as text is typed. However, if you manually position the caret at a different location in the Document and then start typing, you would start a new series of related events.

A class called CompoundEdit already exists and allows us to combine individual edits events into one. All we need to do is customize the UndoManger and implements the rules for combining these individual edits. The CompoundUndoManager class tries to implement these rules the best it can. Undo/redo support is then added to a text component with the following line of code:

CompoundUndoManager um = new CompoundUndoManager(textComponent);

You can then create menu items or buttons to invoke the undo/redo functionality of the UndoManager using the Actions provided:

JButton undo = new JButton(um.getUndoAction());
JButton redo = new JButton(um.getRedoAction());

Add the text component and the buttons to your GUI and you are ready to go.

Compound-Undo-Manager

As you can see from the above image, the Webstart demo will show how undo/redo is handled when using syntax highlighting and other attribute changes like bold, italics and underlining.

Try The Demo

Launch – Using Java™ Web Start (JRE 6 required)

Get The Code

CompoundUndoManager.java

Related Reading

Implementing Undo and Redo
How To Use Actions
Java API: javax.swing.undo.UndoManager

About these ads

20 Responses to “Compound Undo Manager”

  1. Jos said

    Hi Camickr,

    whenever I read your work I get associations of me banging my head against a thick concrete wall while you simply walk around it and enter through a wide open back door. I like your site!

    kind regards,

    Jos

  2. daveg said

    Hey, this is incredibly helpful for my project! I was about to undertake this myself, and then discovered that you have shared an example already.

    Even though there’s no copyright on your code, I’m asking out of courtesy if I can reuse your code verbatim.

    Thanks!

    – dave

  3. bob said

    Note: Bobs multiple comments have been edited into a single comment, Rob

    1) Im finding that cant redo exception is being thrown quite alot. Not sure why.

    2) OK commenting out the following got rid of my CantRedoExceptions…

    // Always start a new compound edit after an undo
    compoundEdit = null;

    commented this out and my redo works fine now.

    3) Also caret position in the document is getting corrupted on redo.

    4) Yeah had to remove all your refs to setCaretPosition, they were causing this

    javax.swing.text.BadLocationException: Position not represented by view

    Corruption and then the JTextPane became unusable.

    5) Generally just not working.

    • Rob Camick said

      Sorry you are having problems. I have updated the entry to include a Webstart app showing how I tested the class. I do not encounter the problems you describe. I did however make a small change in how attributes are undone. If you still encounter problems with the demo then a description on how to reproduce the problem would be helpfull for me to narrow down the problem. Maybe use the Contact Us page where you can provide more detail with the explanation.

  4. Anonymous said

    Many thanks for the code.

    I’d like to incorporate most of it, with changes to http://code.google.com/p/jsyntaxpane</a&lt;

    Any restrictions? Would you like to be attributed in any particular way?

  5. Jörg said

    Hello Rob,

    thanks for your code first of all.
    I made up a little test case which works perfectly as long as I type into the empty textArea. But if after startup I first read a file into the textArea, the menuItems never get enabled after my edits. Do you have a workaround for that?
    You will also see in the code that for a localized application one cannot use myButton.setAction(…) after creation of the button with text. Nothing serious, but you might want to mention it somewhere.

    Greetings

    Jörg

    • Rob Camick said

      Jorg,

      Thanks for posting the code to demonstrate the problem (I removed it to keep the comments shorter).

      The undo listener is added to the Document. When you use the read(…) method of the text area a new Document is created so the initial listener is lost. The solution in your case would be:


      BufferedReader infile = new BufferedReader(...);
      ta.read(infile, null);
      infile.close();
      CompoundUndoManager um = new CompoundUndoManager(ta);
      undoItem.setAction(um.getUndoAction());
      redoItem.setAction(um.getRedoAction());

  6. Jörg said

    Thank you, Rob, that works.
    Just one observation: After reading and displaying the file, my first edit is the deletion of a word. If I use the “BackSpace” key of the keyboard (to remove the characters of a word one at a time), an Undo restores the whole word as expected. If I use the “Delete” key of the editing keypad, however, an Undo restores only one character at a time.

    All the best

    Jörg

    • Rob Camick said

      Yes I tried to mimic the behavour of MS Word as best as I could. This is the behaviour in my version of Word. Basically a continuous edit is one where the position of the caret changes by +/- 1 each time. When you use the “Delete” key the caret does not change position so it is considered a separate edit.

      If you don’t like this behaviour you can try changing the logic that consolidates edits by also including these types of edits, but I will leave the default behaviour as it.

  7. Andy said

    Hi Rob,

    Thanks for this, it’s just what I wanted, well. I’ve implemented it into a small program I made with a JEditorPane. I intialize the CompoundUndoManager when I create the component first – this works great! The problem I have is during the program I set the JEditorPane’s document again and when I do this, the CompoundUndoManager doesn’t work. I’ve treid everything I can think of to update the document used in the CompoundUndoManager but nothing I do seems to work! Have you any idea’s on how I can accomplish what I’m trying to do?

    Thanks in Advance
    Andy

    • Rob Camick said

      Yes, this class is not that smart. The listener is added to the Document. So if you change the Document (I haven’t tested this but) you should probably do a couple of things:

      a) invoke the discardAllEdits() on the undo manager

      b) remove the listener from the old Document:

      textComponent.getDocument().removeUndoableEditListener( um );

      c) add the listener to the new document:

      textComponent.getDocument().addUndoableEditListener( um );

      • Andy said

        Thanks rob,

        I could of sworn I did that before, but obviously I didn’t! Anyway, it worked!

        You are truely a genius. I hope you don’t mind me implementing your work in my software!?

        With thanks and kind regards,
        Andy

  8. Andy said

    Hi it’s me again (Sorry),

    Now I’ve got my first problemn sorted I’ve stumbled accross a second problem…

    When I change a document and discard all edits, the JMenuItem still stays enabled, until I click it – when it realizes there’s nothing to undo! Again, I’ve tried everythinhg I can possibly imagine but nothing works!

    Any Idea’s,
    Thanks in Advance,
    Andy

    • Rob Camick said

      The Action used by the menu item is contained within the CompoundUndoManager class. So I would suggest you need to modify this class to include a “reset” method. This method would include code from your first problem as well as this problem.

      To solve this problem I would guess you would then need to add code like:

      undoAction.setEnabled( undoManager.canUndo() );
      redoAction.setEnabled( undoManager.canRedo() );

      If it works you can email me the code for your method and I’ll look at maybe adding it to the base class when I get time.

      • Andy said

        I had tried this, but I just realised why I didn’t work when I last tried it – I put the code in a method that I wasn’t using anymore! Anyway, I created a new a method called ‘reset’ in your class with the two lines of code in you said, and it works when I call it after the dicard all edits!

        I guess you don’t need me to email the code if I’ve just done what you told me too, but if you still want me too – I will (just ask me again)

        With thanks and kind regards
        Andy

  9. Erwin said

    Hey, nice code, thanks!

    I find very weird that the caret position is not updated anyway after an Undo or Redo :)

  10. Kanishka said

    Hi Rob,Thanks for the Compound UndoManager class.
    My requirement is a bit odd though.
    I have two jtextpanes in my GUI and only one set of undo redo buttons. I am trying to use the same undo redo buttons for both text components without success.
    CompoundUndoManager um = new CompoundUndoManager(component1);
    CompoundUndoManager um2 = new CompoundUndoManager(component2);

    undo.addActionListener(um.getUndoAction());
    redo.addActionListener(um.getRedoAction());

    undo.addActionListener(um2.getUndoAction());
    redo.addActionListener(um2.getRedoAction());

    Any ideas/suggestions how to get this working?

    KR

    • Rob Camick said

      I’m not sure the best way to do this. One way is to rest the ActionListener of the buttons every time you change focus from one text pane to the other.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.

Join 97 other followers

%d bloggers like this: