Search code examples
javaswingjtextareaundoredo

Using a single JTextArea with multiple UndoManagers


I have a JTextArea and a JComboBox to allow me to cycle through various open files - the contents of the JTextArea change as I select a different file. I am trying to maintain a different Undo buffer per file and have defined a separate UndoManager per file.

I have created a simpler SSCCE to demonstrate my problem using two buffers, which I call "One" and "Two" - with a simple button to switch between them. Once an UndoableEdit happens, it checks the active buffer and performs an addEdit() on the respective UndoManager. When the "Undo" button is pressed, then it checks canUndo() and performs an undo() on the respective UndoManager. I have a flag called ignoreEdit, which is used when switching between buffers to ignore those edits from being recorded.

If I never switch between the buffers, then I don't have a problem, Undo works as expected. It is only when I switch between the buffers and appear to "break" the Document, does it fail. The following steps can be used to recreate the problem:

In buffer "One", type:

THIS
IS ONE
EXAMPLE

Switch to buffer "Two", type:

THIS
IS ANOTHER
EXAMPLE

Switch to buffer "One" and press the "Undo" button multiple times. After a few Undo operations, the buffer looks like this (with no way for the cursor to select the first two lines). However, the contents of textArea.getText() are correct as per the System.out.println() - so, it looks like a rendering issue?

THIS

THISIS ONE

This can't be the first time that someone has tried to implement independent Undo buffers per file? I am obviously doing something wrong with the Document model and inherently breaking it, but I looking for some advice on how best to fix this?

The code for the SSCCE is included below:

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

public class SSCCE extends JFrame implements ActionListener, UndoableEditListener {
  private final JLabel labTextArea;
  private final JTextArea textArea;
  private final JScrollPane scrollTextArea;
  private final Document docTextArea;
  private final JButton bOne, bTwo, bUndo;
  private final UndoManager uOne, uTwo;
  private String sOne, sTwo;
  private boolean ignoreEdit = false;

  public SSCCE(String[] args) {
    setTitle("SSCCE - Short, Self Contained, Correct Example");
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setSize(300, 200);
    setLocationRelativeTo(null);

    labTextArea = new JLabel("One");
    getContentPane().add(labTextArea, BorderLayout.PAGE_START);

    uOne = new UndoManager();
    uTwo = new UndoManager();
    sOne = new String();
    sTwo = new String();

    textArea = new JTextArea();
    docTextArea = textArea.getDocument();
    docTextArea.addUndoableEditListener(this);
    scrollTextArea = new JScrollPane(textArea);
    getContentPane().add(scrollTextArea, BorderLayout.CENTER);

    JPanel pButtons = new JPanel();
    bOne = new JButton("One");
    bOne.addActionListener(this);
    bOne.setFocusable(false);
    pButtons.add(bOne, BorderLayout.LINE_START);
    bTwo = new JButton("Two");
    bTwo.addActionListener(this);
    bTwo.setFocusable(false);
    pButtons.add(bTwo, BorderLayout.LINE_END);
    bUndo = new JButton("Undo");
    bUndo.addActionListener(this);
    bUndo.setFocusable(false);
    pButtons.add(bUndo, BorderLayout.LINE_END);
    getContentPane().add(pButtons, BorderLayout.PAGE_END);

    setVisible(true);
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    if (e.getSource().equals(bOne)) {
      if (!labTextArea.getText().equals("One")) {
        sTwo = textArea.getText();
        ignoreEdit = true;
        textArea.setText(sOne);
        ignoreEdit = false;
        labTextArea.setText("One");
      }
    }
    else if (e.getSource().equals(bTwo)) {
      if (!labTextArea.getText().equals("Two")) {
        sOne = textArea.getText();
        ignoreEdit = true;
        textArea.setText(sTwo);
        ignoreEdit = false;
        labTextArea.setText("Two");
      }
    }
    else if (e.getSource().equals(bUndo)) {
      if (labTextArea.getText().equals("One")) {
        try {
          if (uOne.canUndo()) {
            System.out.println("Performing Undo for One");
            uOne.undo();
            System.out.println("Buffer One is now:\n" + textArea.getText() + "\n");
          }
          else {
            System.out.println("Nothing to Undo for One");
          }
        }
        catch (CannotUndoException ex) {
          ex.printStackTrace();
        }
      }
      else if (labTextArea.getText().equals("Two")) {
        try {
          if (uTwo.canUndo()) {
            System.out.println("Performing Undo for Two");
            uTwo.undo();
            System.out.println("Buffer Two is now:\n" + textArea.getText() + "\n");
          }
          else {
            System.out.println("Nothing to Undo for Two");
          }
        }
        catch (CannotUndoException ex) {
          ex.printStackTrace();
        }
      }
    }
  }

  @Override
  public void undoableEditHappened(UndoableEditEvent e) {
    if (!ignoreEdit) {
      if (labTextArea.getText().equals("One")) {
        System.out.println("Adding Edit for One");
        uOne.addEdit(e.getEdit());
      }
      else if (labTextArea.getText().equals("Two")) {
        System.out.println("Adding Edit for Two");
        uTwo.addEdit(e.getEdit());
      }
    }
  }

  public static void main(final String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        new SSCCE(args);
      }
    });
  }
}

Solution

  • Previously I had attempted to create a new instance of the Document class (each referencing the same Undo listener) and was going to use JTextArea.setDocument() instead of JTextArea.setText(). However, Document is an interface and can't be instantiated, but after reading the reference that mKorbel posted, I tried this using the PlainDocument class instead, which worked.

    I have decided to maintain a HashMap<String, Document> to contain my Document classes and switch between them. When I switch the Document, I don't see the Undo/Redo issue - I suppose as I am no longer breaking the Document.

    Updated SSCCE below which now uses JTextArea.setDocument() instead of JTextArea.setText(). This also has the advantage of not requiring the ignoreEdit boolean as setDocument() doesn't trigger an UndoableEditEvent, whereas setText() does. Each Document then references the local classes UndoableEditListener.

    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.event.*;
    import javax.swing.text.*;
    import javax.swing.undo.*;
    
    public class SSCCE extends JFrame implements ActionListener, UndoableEditListener {
      private final JLabel labTextArea;
      private final JTextArea textArea;
      private final JScrollPane scrollTextArea;
      private final Document docTextArea;
      private final JButton bOne, bTwo, bUndo;
      private final UndoManager uOne, uTwo;
      private Document dOne, dTwo;
    
      public SSCCE(String[] args) {
        setTitle("SSCCE - Short, Self Contained, Correct Example");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(300, 200);
        setLocationRelativeTo(null);
    
        labTextArea = new JLabel("One");
        getContentPane().add(labTextArea, BorderLayout.PAGE_START);
    
        uOne = new UndoManager();
        uTwo = new UndoManager();
        dOne = new PlainDocument();
        dTwo = new PlainDocument();
        dOne.addUndoableEditListener(this);
        dTwo.addUndoableEditListener(this);
    
        textArea = new JTextArea();
        docTextArea = textArea.getDocument();
        docTextArea.addUndoableEditListener(this);
        textArea.setDocument(dOne);
        scrollTextArea = new JScrollPane(textArea);
        getContentPane().add(scrollTextArea, BorderLayout.CENTER);
    
        JPanel pButtons = new JPanel();
        bOne = new JButton("One");
        bOne.addActionListener(this);
        bOne.setFocusable(false);
        pButtons.add(bOne, BorderLayout.LINE_START);
        bTwo = new JButton("Two");
        bTwo.addActionListener(this);
        bTwo.setFocusable(false);
        pButtons.add(bTwo, BorderLayout.LINE_END);
        bUndo = new JButton("Undo");
        bUndo.addActionListener(this);
        bUndo.setFocusable(false);
        pButtons.add(bUndo, BorderLayout.LINE_END);
        getContentPane().add(pButtons, BorderLayout.PAGE_END);
    
        setVisible(true);
      }
    
      @Override
      public void actionPerformed(ActionEvent e) {
        if (e.getSource().equals(bOne)) {
          if (!labTextArea.getText().equals("One")) {
            textArea.setDocument(dOne);
            labTextArea.setText("One");
          }
        }
        else if (e.getSource().equals(bTwo)) {
          if (!labTextArea.getText().equals("Two")) {
            textArea.setDocument(dTwo);
            labTextArea.setText("Two");
          }
        }
        else if (e.getSource().equals(bUndo)) {
          if (labTextArea.getText().equals("One")) {
            try {
              if (uOne.canUndo()) {
                System.out.println("Performing Undo for One");
                uOne.undo();
                System.out.println("Buffer One is now:\n" + textArea.getText() + "\n");
              }
              else {
                System.out.println("Nothing to Undo for One");
              }
            }
            catch (CannotUndoException ex) {
              ex.printStackTrace();
            }
          }
          else if (labTextArea.getText().equals("Two")) {
            try {
              if (uTwo.canUndo()) {
                System.out.println("Performing Undo for Two");
                uTwo.undo();
                System.out.println("Buffer Two is now:\n" + textArea.getText() + "\n");
              }
              else {
                System.out.println("Nothing to Undo for Two");
              }
            }
            catch (CannotUndoException ex) {
              ex.printStackTrace();
            }
          }
        }
      }
    
      @Override
      public void undoableEditHappened(UndoableEditEvent e) {
        if (labTextArea.getText().equals("One")) {
          System.out.println("Adding Edit for One");
          uOne.addEdit(e.getEdit());
        }
        else if (labTextArea.getText().equals("Two")) {
          System.out.println("Adding Edit for Two");
          uTwo.addEdit(e.getEdit());
        }
      }
    
      public static void main(final String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            new SSCCE(args);
          }
        });
      }
    }