swing – CompoundUndoManager in Java

The problem of ‘your code’ is not in your code, it’s inside the CompoundUndoManager. At the current version, this will already throw lots and lots of uncaught exceptions. Check your app’s console output while typing.

So to fix your problem(s), you might have to severely adapt that CompoundUndoManager.

Below is the code for a cleaner and a more simple version of your code, along with a lot of hints, and an in-window tracking of the exceptions:

package stackoverflow.compoundundo;

import java.awt.BorderLayout;
import java.awt.Color;
import java.time.LocalTime;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.border.TitledBorder;


/*
 * let's use the class itself as window.
 * Thus all window/UI operations can be directly accessed from the outside.
 * This is usually the desired behaviour, unless of course there's a reason for hiding access to the window
 */
public class MainWindow extends JFrame {
    private static final long serialVersionUID = 8381482782072686558L;



    static {
        try { // make window look like default OS windows
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (final Exception e) { /* we can ignore this */ }
    }


    // separate static stuff from non-static stuff
    public static void main(final String[] args) {
        final MainWindow gui = new MainWindow();
        gui.setVisible(true); // should be called from outside
    }



    // initialize everything ASAP, especially in windows
    // inheritance and access to member variables can become a real pain otherwise, when components are null
    private final JTextArea     textArea        = new JTextArea();
    private final JTextField    errorTextfield  = new JTextField();
    //  KeyHandler kHandler = new KeyHandler(this); // I do not have that class; you may just comment it in again
    private final CompoundUndoManager undoManager = new CompoundUndoManager(textArea); // use full name for member variables

    // also, we probably will not need access to iUndo, menuEdit, scrollPane etc,
    // so there's no need (yet) to have those as member variables

    public MainWindow() {
        setTitle("Notepad");
        setLayout(new BorderLayout());
        //      setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // this is a bad idea in most cases, as it
        // may interrupt a lot of other threads and possibly breaking save files that some other threads might be writing to etc
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // this is much cleaner to use
        setSize(800, 600);

        // menu bar
        final JMenu menuEdit = new JMenu("Edit");

        final JMenuItem iUndo = new JMenuItem("Undo");
        iUndo.addActionListener(e -> gMenuUndo_Clicked());
        /*
         * instead of collecting all events in one class, and then having to separate them,
         * we will point our actions directly to methods. this reduces work, provides clarity in code
         * and best of all: this hides access to internal functions from the outside world, because no public methods are needed
         * like they would be when implementing classes like ActionListener (public class GUI implements ActionListener {).
         * this prevents corruptive access from the outside, but can easily be allowed by making the methods (gMenuUndo_Clicked) public
         */
        iUndo.setActionCommand("Undo");
        menuEdit.add(iUndo);

        final JMenuItem iRedo = new JMenuItem("Redo");
        iRedo.addActionListener(e -> gMenuRedo_Clicked());
        iRedo.setActionCommand("Redo");
        menuEdit.add(iRedo);

        final JMenuBar menuBar = new JMenuBar();
        menuBar.add(menuEdit);
        setJMenuBar(menuBar);

        //      textArea.addKeyListener(kHandler); // I do not have that class; you may just comment it in again
        textArea.setBorder(new TitledBorder("My Text")); // makes it a bit more beautiful

        final JScrollPane scrollPane = new JScrollPane(textArea, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); // we might
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        add(scrollPane, BorderLayout.CENTER);

        {
            final JPanel bottom = new JPanel(new BorderLayout());
            bottom.add(new JLabel("Time/Error: "), BorderLayout.WEST);
            errorTextfield.setForeground(Color.RED);
            bottom.add(errorTextfield, BorderLayout.CENTER);
            add(bottom, BorderLayout.SOUTH);
        }

        // addition: log all errors that happen,
        // because CompoundUndoManager is a troublesome class, where all the exceptions will occur
        Thread.setDefaultUncaughtExceptionHandler((t, e) -> logError(e));
    }



    private void gMenuRedo_Clicked() {
        // UI interactions should usually handle exceptions
        try {
            undoManager.redo();
        } catch (final Exception e) {
            logError(e);
        }
    }

    private void gMenuUndo_Clicked() {
        try {
            undoManager.undo();
        } catch (final Exception e) {
            logError(e);
        }
    }

    private void logError(final Throwable e) {
        e.printStackTrace();
        //      errorTextfield.setText(LocalDateTime.now() + ": " + e); // we do not need date
        errorTextfield.setText(LocalTime.now() + ": " + e); // include time to see when new exceptions happen
        errorTextfield.setSelectionStart(0);
        errorTextfield.setSelectionEnd(0);
    }



}

Update

On typing (2+ letters) I get this error:

java.lang.ClassCastException: class javax.swing.text.AbstractDocument$DefaultDocumentEventUndoableWrapper cannot be cast to class javax.swing.text.AbstractDocument$DefaultDocumentEvent (javax.swing.text.AbstractDocument$DefaultDocumentEventUndoableWrapper and javax.swing.text.AbstractDocument$DefaultDocumentEvent are in module java.desktop of loader 'bootstrap')
    at stackoverflow.compoundundo.CompoundUndoManager.undoableEditHappened(CompoundUndoManager.java:89)
    at java.desktop/javax.swing.text.AbstractDocument.fireUndoableEditUpdate(AbstractDocument.java:293)
    at java.desktop/javax.swing.text.AbstractDocument.handleInsertString(AbstractDocument.java:761)
    at java.desktop/javax.swing.text.AbstractDocument.insertString(AbstractDocument.java:716)
    at java.desktop/javax.swing.text.PlainDocument.insertString(PlainDocument.java:131)
    at java.desktop/javax.swing.text.AbstractDocument.replace(AbstractDocument.java:675)
    at java.desktop/javax.swing.text.JTextComponent.replaceSelection(JTextComponent.java:1339)
    at java.desktop/javax.swing.text.DefaultEditorKit$DefaultKeyTypedAction.actionPerformed(DefaultEditorKit.java:884)
    at java.desktop/javax.swing.SwingUtilities.notifyAction(SwingUtilities.java:1810)
    at java.desktop/javax.swing.JComponent.processKeyBinding(JComponent.java:2900)
    at java.desktop/javax.swing.JComponent.processKeyBindings(JComponent.java:2948)
    at java.desktop/javax.swing.JComponent.processKeyEvent(JComponent.java:2862)
    at java.desktop/java.awt.Component.processEvent(Component.java:6412)
    at java.desktop/java.awt.Container.processEvent(Container.java:2263)
    at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5011)
    at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2321)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4843)
    at java.desktop/java.awt.KeyboardFocusManager.redispatchEvent(KeyboardFocusManager.java:1950)
    at java.desktop/java.awt.DefaultKeyboardFocusManager.dispatchKeyEvent(DefaultKeyboardFocusManager.java:870)
    at java.desktop/java.awt.DefaultKeyboardFocusManager.preDispatchKeyEvent(DefaultKeyboardFocusManager.java:1139)
    at java.desktop/java.awt.DefaultKeyboardFocusManager.typeAheadAssertions(DefaultKeyboardFocusManager.java:1009)
    at java.desktop/java.awt.DefaultKeyboardFocusManager.dispatchEvent(DefaultKeyboardFocusManager.java:835)
    at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:4892)
    at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2321)
    at java.desktop/java.awt.Window.dispatchEventImpl(Window.java:2772)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4843)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:772)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:95)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:745)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:743)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

and when trying the undo button (2+ times) I get those errors:

javax.swing.undo.CannotUndoException
    at java.desktop/javax.swing.undo.UndoManager.tryUndoOrRedo(UndoManager.java:468)
    at java.desktop/javax.swing.undo.UndoManager.undo(UndoManager.java:416)
    at stackoverflow.compoundundo.CompoundUndoManager.undo(CompoundUndoManager.java:58)
    at stackoverflow.compoundundo.MainWindow.gMenuUndo_Clicked(MainWindow.java:123)
    at stackoverflow.compoundundo.MainWindow.lambda$0(MainWindow.java:70)
    at java.desktop/javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1967)
    at java.desktop/javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2308)
    at java.desktop/javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:405)
    at java.desktop/javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:262)
    at java.desktop/javax.swing.AbstractButton.doClick(AbstractButton.java:369)
    at java.desktop/javax.swing.plaf.basic.BasicMenuItemUI.doClick(BasicMenuItemUI.java:1020)
    at java.desktop/javax.swing.plaf.basic.BasicMenuItemUI$Handler.mouseReleased(BasicMenuItemUI.java:1064)
    at java.desktop/java.awt.Component.processMouseEvent(Component.java:6635)
    at java.desktop/javax.swing.JComponent.processMouseEvent(JComponent.java:3342)
    at java.desktop/java.awt.Component.processEvent(Component.java:6400)
    at java.desktop/java.awt.Container.processEvent(Container.java:2263)
    at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5011)
    at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2321)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4843)
    at java.desktop/java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4918)
    at java.desktop/java.awt.LightweightDispatcher.processMouseEvent(Container.java:4547)
    at java.desktop/java.awt.LightweightDispatcher.dispatchEvent(Container.java:4488)
    at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2307)
    at java.desktop/java.awt.Window.dispatchEventImpl(Window.java:2772)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4843)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:772)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:95)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:745)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:743)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

And for every undo action it removes only 1 letter for me.

This is all on

JRE Version:                11.0.10
JRE JMX Version:            11.0.10+8-jvmci-21.0-b06
JRE Version (verbose):          Java 11.0.10 64bit
JRE Version (verbose, hardware):    Java 11.0.10 64bit on Windows Server 2016 hardware
JRE Home:               D:appsgraalvmgraalvm-ce-java11-21.0.0.2
JRE Vendor:             GraalVM Community

System Architecture:            amd64
System Name:                Windows Server 2016
System Version:             10.0
Archtiecture Bits:          64
Available Processors:           2

So, when running it under

JRE Version:                16-ea
JRE JMX Version:            16-ea+1-3
JRE Version (verbose):          Java 16-ea 64bit
JRE Version (verbose, hardware):    Java 16-ea 64bit on Windows Server 2016 hardware
JRE Home:               D:appsjavajdk-16
JRE Vendor:             Oracle Corporation

System Architecture:            amd64
System Name:                Windows Server 2016
System Version:             10.0
Archtiecture Bits:          64
Available Processors:           2

I also get no errors, and it also removes all the text I typed since last entered/clicked the text box.

So now you could use your keylistener, and every time the user hits the space bar or some foreign letters, you could trigger the undoManager to accept this as new state. My guess is you invoke the undoManager.changedUpdate method, but you might have to look that up in the documentation.

Leave a Comment