Menu

Add Undo/Redo Support to Your SWT StyledText-s

Once again, I found a nice tool (namely the Regex Util plug-in, thanks for sharing! :)) for Eclipse, which is working well but is missing one fundamental feature...

For me, this feature is the wide-spread Undo-Redo functionality in text fields and areas. The fact that this functionality is missing is not really a bug of the plug-in, it's just that the SWT StyledText component which the plug-in utilizes simply doesn't support Undo-Redo.

Path to solution

And once again, I have found it surprisingly easy to implement this feature. The solution doesn't integrate with the Eclipse's editing history (the 'Edit' menu) and it doesn't provide pop-up menu contributions. But this is not really necessary when you just want to have Ctrl+Z and Ctrl+Y functioning in particular text fields of a plug-in view.

After lots of googling, I found only one example code (SWT Undo Redo) on how to implement it. And after several "WTF"-s, I concluded that it is really just an example - a concept which the reader himself is expected to make functional. So I did...

Enough theory, show me the code

Together with a little abstraction of the Undo-Redo stack (later, we could e. g. extend it with a logic of limiting maximum Undo steps count), we can put all the logic in just one class like this:

package etinyplugins.commons.swt;

import java.util.Stack;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ExtendedModifyEvent;
import org.eclipse.swt.custom.ExtendedModifyListener;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;

/**
 * Adds the Undo-Redo functionality (working Ctrl+Z and Ctrl+Y) to an instance
 * of {@link StyledText}.
 * 
 * @author Petr Bodnar
 * @see {@linkplain http
 *      ://www.java2s.com/Code/Java/SWT-JFace-Eclipse/SWTUndoRedo.htm} -
 *      inspiration for this code, though not really functioning - it mainly
 *      shows which listeners to use...
 * @see {@linkplain http
 *      ://stackoverflow.com/questions/7179464/swt-how-to-recreate
 *      -a-default-context-menu-for-text-fields} -
 *      "SWT's StyledText doesn't support Undo-Redo out-of-the-box"
 */
public class UndoRedoImpl implements KeyListener, ExtendedModifyListener {

    /**
     * Encapsulation of the Undo and Redo stack(s).
     */
    private static class UndoRedoStack<T> {

        private Stack<T> undo;
        private Stack<T> redo;

        public UndoRedoStack() {
            undo = new Stack<T>();
            redo = new Stack<T>();
        }

        public void pushUndo(T delta) {
            undo.add(delta);
        }

        public void pushRedo(T delta) {
            redo.add(delta);
        }

        public T popUndo() {
            T res = undo.pop();
            return res;
        }

        public T popRedo() {
            T res = redo.pop();
            return res;
        }

        public void clearRedo() {
            redo.clear();
        }

        public boolean hasUndo() {
            return !undo.isEmpty();
        }

        public boolean hasRedo() {
            return !redo.isEmpty();
        }

    }

    private StyledText editor;

    private UndoRedoStack<ExtendedModifyEvent> stack;

    private boolean isUndo;

    private boolean isRedo;

    /**
     * Creates a new instance of this class. Automatically starts listening to
     * corresponding key and modify events coming from the given
     * <var>editor</var>.
     * 
     * @param editor
     *            the text field to which the Undo-Redo functionality should be
     *            added
     */
    public UndoRedoImpl(StyledText editor) {
        editor.addExtendedModifyListener(this);
        editor.addKeyListener(this);

        this.editor = editor;
        stack = new UndoRedoStack<ExtendedModifyEvent>();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.eclipse.swt.events.KeyListener#keyPressed(org.eclipse.swt.events.
     * KeyEvent)
     */
    public void keyPressed(KeyEvent e) {
        // Listen to CTRL+Z for Undo, to CTRL+Y or CTRL+SHIFT+Z for Redo
        boolean isCtrl = (e.stateMask & SWT.CTRL) > 0;
        boolean isAlt = (e.stateMask & SWT.ALT) > 0;
        if (isCtrl && !isAlt) {
            boolean isShift = (e.stateMask & SWT.SHIFT) > 0;
            if (!isShift && e.keyCode == 'z') {
                undo();
            } else if (!isShift && e.keyCode == 'y' || isShift
                    && e.keyCode == 'z') {
                redo();
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.eclipse.swt.events.KeyListener#keyReleased(org.eclipse.swt.events
     * .KeyEvent)
     */
    public void keyReleased(KeyEvent e) {
        // ignore
    }

    /**
     * Creates a corresponding Undo or Redo step from the given event and pushes
     * it to the stack. The Redo stack is, logically, emptied if the event comes
     * from a normal user action.
     * 
     * @param event
     * @see org.eclipse.swt.custom.ExtendedModifyListener#modifyText(org.eclipse.
     *      swt.custom.ExtendedModifyEvent)
     */
    public void modifyText(ExtendedModifyEvent event) {
        if (isUndo) {
            stack.pushRedo(event);
        } else { // is Redo or a normal user action
            stack.pushUndo(event);
            if (!isRedo) {
                stack.clearRedo();
                // TODO Switch to treat consecutive characters as one event?
            }
        }
    }

    /**
     * Performs the Undo action. A new corresponding Redo step is automatically
     * pushed to the stack.
     */
    private void undo() {
        if (stack.hasUndo()) {
            isUndo = true;
            revertEvent(stack.popUndo());
            isUndo = false;
        }
    }

    /**
     * Performs the Redo action. A new corresponding Undo step is automatically
     * pushed to the stack.
     */
    private void redo() {
        if (stack.hasRedo()) {
            isRedo = true;
            revertEvent(stack.popRedo());
            isRedo = false;
        }
    }

    /**
     * Reverts the given modify event, in the way as the Eclipse text editor
     * does it.
     * 
     * @param event
     */
    private void revertEvent(ExtendedModifyEvent event) {
        editor.replaceTextRange(event.start, event.length, event.replacedText);
        // (causes the modifyText() listener method to be called)

        editor.setSelectionRange(event.start, event.replacedText.length());
    }

}

And all one needs to change in an existing code base is to add a call to the constructor of UndoRedoImpl, passing it the instance of the to-be-extended editor (text):

new UndoRedoImpl(text /* instance of StyledText */);

Yes, it really is that simple. The SWT API-s for the modify event and for text manipulations go together nicely.

And what do YOU think? Can't there be any hidden gotchas in this solution?

Posted by Petr Bodnár 2013-02-04 Labels: SWT StyledText undo redo
  • Anonymous

    Anonymous - 2014-01-07

    Nice code. An alternative is to wrap it with a TextViewer and use the JFace TextViewerUndoManager.

    Jan

     
    • Petr Bodnár

      Petr Bodnár - 2014-05-23

      Jan, thanks for the tip, it looks viable. One should only be aware that "Clients are supposed to instantiate a text viewer and subsequently to communicate with it exclusively using the ITextViewer interface or any of the implemented extension interfaces." (taken from javadoc).

      And sorry for the late approval of your comment, just noticed now the need to approve comments... :)

       
  • Anonymous

    Anonymous - 2016-03-27

    Hi, thanks for this discussion which I only found out about now.

    I'm currently working on a rich text editor for swt that should even include hyperlink ability.

    The problem that I see with either of the proposed solutions is that neither of them takes into account the styled ranges. They only replace the text but if I for example had a removed text in bold and then I undo my removal the text will be reappear normal not bold.

    This is something that for example Microsoft Word can do: restore the rich text attributes and a good solution should be able to do that as well.

    I'm not sure yet which way to go but probably both of the mentioned possibilies could work: either write one's own solution or use the TextViewerUndoManager and register a listener there. Any way cache the style ranges at undo and reinsert them at redo into the StyledText.

    Günther

     
    • Petr Bodnár

      Petr Bodnár - 2016-11-25

      Hi Günther, thanks for sharing your thoughts. The extension you propose seems like a pretty nice challenge, but probably doable - one "only" needs to pick up the applied StyleRange-s from inside the text change listener and reapply them on undo/redo. Have you managed to implement some reasonable solution?

       

Anonymous
Anonymous

Add attachments
Cancel