JTextArea setText() & UndoManager
Asked Answered
L

6

4

I'm using an UndoManager to capture changes in my JTextArea.

The method setText() however deletes everything and then pastes the text. When I undo I firstly see an empty area and then it would show which text it had before.

How to reproduce:

  1. Run the following code
  2. Click the setText() button
  3. Press CTRL+Z to undo (you'll see an empty textarea!)
  4. Press CTRL+Z to undo (you'll see the actual previous text)

I want to skip 3).

import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.Document;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;

import java.awt.event.ActionEvent;
import javax.swing.JButton;
import java.awt.event.ActionListener;

@SuppressWarnings("serial")
public class JTextComponentSetTextUndoEvent extends JFrame
{
    JTextArea area = new JTextArea();

    public JTextComponentSetTextUndoEvent()
    {
        setSize(300, 300);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        getContentPane().setLayout(null);

        area.setText("Test");
        area.setBounds(0, 96, 146, 165);
        getContentPane().add(area);

        JButton btnSettext = new JButton("setText()");
        btnSettext.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent arg0)
            {
                area.setText("stackoverflow.com");
            }
        });
        btnSettext.setBounds(0, 28, 200, 50);
        getContentPane().add(btnSettext);

        final UndoManager undoManager = new UndoManager();
        Document doc = area.getDocument();

        doc.addUndoableEditListener(new UndoableEditListener()
        {
            public void undoableEditHappened(UndoableEditEvent evt)
            {
                undoManager.addEdit(evt.getEdit());
            }
        });

        area.getActionMap().put("Undo", new AbstractAction("Undo")
        {
            public void actionPerformed(ActionEvent evt)
            {
                try
                {
                    if (undoManager.canUndo())
                    {
                        undoManager.undo();
                    }
                } catch (CannotUndoException e)
                {
                }
            }
        });

        area.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo");

        area.getActionMap().put("Redo", new AbstractAction("Redo")
        {
            public void actionPerformed(ActionEvent evt)
            {
                try
                {
                    if (undoManager.canRedo())
                    {
                        undoManager.redo();
                    }
                } catch (CannotRedoException e)
                {
                }
            }
        });

        area.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo");
    }

    public static void main(String[] args)
    {
        new JTextComponentSetTextUndoEvent().setVisible(true);
    }
}
Lanthanide answered 26/6, 2014 at 14:23 Comment(5)
For better help sooner, post an MCVE (Minimal Complete and Verifiable Example).Radbourne
@Andrew Thompson: MCVE code added.Lanthanide
If the problem Always occurs why not call .undo in the code twice?Noddle
@Roan: It only occurs when setting the text but not when editing it by hand.Lanthanide
hmm maybe undo once do getText to see if the area is empty if so undo again else do nothing?Noddle
D
2

You can try something like this:

//Works fine for me on Windows 7 x64 using JDK 1.7.0_60:
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;

public final class UndoManagerTest {
  private final JTextField textField0 = new JTextField("default");
  private final JTextField textField1 = new JTextField();
  private final UndoManager undoManager0 = new UndoManager();
  private final UndoManager undoManager1 = new UndoManager();

  public JComponent makeUI() {
    textField1.setDocument(new CustomUndoPlainDocument());
    textField1.setText("aaaaaaaaaaaaaaaaaaaaa");

    textField0.getDocument().addUndoableEditListener(undoManager0);
    textField1.getDocument().addUndoableEditListener(undoManager1);

    JPanel p = new JPanel();
    p.add(new JButton(new AbstractAction("undo") {
      @Override public void actionPerformed(ActionEvent e) {
        if (undoManager0.canUndo()) {
          undoManager0.undo();
        }
        if (undoManager1.canUndo()) {
          undoManager1.undo();
        }
      }
    }));
    p.add(new JButton(new AbstractAction("redo") {
      @Override public void actionPerformed(ActionEvent e) {
        if (undoManager0.canRedo()) {
          undoManager0.redo();
        }
        if (undoManager1.canRedo()) {
          undoManager1.redo();
        }
      }
    }));
    p.add(new JButton(new AbstractAction("setText(new Date())") {
      @Override public void actionPerformed(ActionEvent e) {
        String str = new Date().toString();
        textField0.setText(str);
        textField1.setText(str);
      }
    }));

    Box box = Box.createVerticalBox();
    box.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    box.add(makePanel("Default", textField0));
    box.add(Box.createVerticalStrut(5));
    box.add(makePanel("replace ignoring undo", textField1));

    JPanel pp = new JPanel(new BorderLayout());
    pp.add(box, BorderLayout.NORTH);
    pp.add(p, BorderLayout.SOUTH);
    return pp;
  }
  private static JPanel makePanel(String title, JComponent c) {
    JPanel p = new JPanel(new BorderLayout());
    p.setBorder(BorderFactory.createTitledBorder(title));
    p.add(c);
    return p;
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
      @Override public void run() {
        createAndShowGUI();
      }
    });
  }
  public static void createAndShowGUI() {
    JFrame f = new JFrame();
    f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    f.getContentPane().add(new UndoManagerTest().makeUI());
    f.setSize(320, 240);
    f.setLocationRelativeTo(null);
    f.setVisible(true);
  }
}

class CustomUndoPlainDocument extends PlainDocument {
  private CompoundEdit compoundEdit;
  @Override protected void fireUndoableEditUpdate(UndoableEditEvent e) {
    if (compoundEdit == null) {
      super.fireUndoableEditUpdate(e);
    } else {
      compoundEdit.addEdit(e.getEdit());
    }
  }
  @Override public void replace(
      int offset, int length,
      String text, AttributeSet attrs) throws BadLocationException {
    if (length == 0) {
      System.out.println("insert");
      super.replace(offset, length, text, attrs);
    } else {
      System.out.println("replace");
      compoundEdit = new CompoundEdit();
      super.fireUndoableEditUpdate(new UndoableEditEvent(this, compoundEdit));
      super.replace(offset, length, text, attrs);
      compoundEdit.end();
      compoundEdit = null;
    }
  }
}
Dividend answered 27/6, 2014 at 11:27 Comment(1)
Right. The overridden replace() method fixes it.Lanthanide
C
7

By default javax.swing.undo.UndoManager retains each undoable edit, including the one that removes of the original text (your step three). Individual edits are inaccessible, but you can group edits using the approach cited here. Some additional notes on your example:

  • For better cross-platform results, use getMenuShortcutKeyMask() as suggested here.

  • Use a layout; if necessary, invoke setSize() after pack(), as shown here.

  • Swing GUI objects should be constructed and manipulated only on the event dispatch thread.

Code:

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.Document;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;

@SuppressWarnings("serial")
public class JTextComponentSetTextUndoEvent extends JFrame {

    private static final int MASK
        = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
    private JTextArea area = new JTextArea();
    private UndoManager undoManager = new UndoManager();

    public JTextComponentSetTextUndoEvent() {
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        area.setText("Test");
        add(area);
        JButton btnSettext = new JButton("setText()");
        btnSettext.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                area.setText("stackoverflow.com");
            }
        });
        add(btnSettext, BorderLayout.PAGE_END);
        Document doc = area.getDocument();
        doc.addUndoableEditListener(new UndoableEditListener() {
            @Override
            public void undoableEditHappened(UndoableEditEvent e) {
                undoManager.addEdit(e.getEdit());
                System.out.println(e);
            }
        });
        area.getActionMap().put("Undo", new AbstractAction("Undo") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                try {
                    if (undoManager.canUndo()) {
                        undoManager.undo();
                    }
                } catch (CannotUndoException e) {
                    System.out.println(e);
                }
            }
        });
        area.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, MASK), "Undo");
        area.getActionMap().put("Redo", new AbstractAction("Redo") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                try {
                    if (undoManager.canRedo()) {
                        undoManager.redo();
                    }
                } catch (CannotRedoException e) {
                    System.out.println(e);
                }
            }
        });
        area.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y,MASK), "Redo");
        pack();
        setSize(320, 240);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new JTextComponentSetTextUndoEvent().setVisible(true);
            }
        });
    }
}
Calciferol answered 26/6, 2014 at 19:0 Comment(0)
Y
3

A simple workaround is to use replaceRange:

area.replaceRange(newText, 0, area.getText().length());

This counts as a single edit, so is undone in one step.

Yazbak answered 31/8, 2015 at 22:16 Comment(1)
his one liner did the trick for me. I'm using a org.fife.ui.rsyntaxtextarea.RSyntaxTextArea, which take care of the undo manager.Wessling
D
2

You can try something like this:

//Works fine for me on Windows 7 x64 using JDK 1.7.0_60:
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;

public final class UndoManagerTest {
  private final JTextField textField0 = new JTextField("default");
  private final JTextField textField1 = new JTextField();
  private final UndoManager undoManager0 = new UndoManager();
  private final UndoManager undoManager1 = new UndoManager();

  public JComponent makeUI() {
    textField1.setDocument(new CustomUndoPlainDocument());
    textField1.setText("aaaaaaaaaaaaaaaaaaaaa");

    textField0.getDocument().addUndoableEditListener(undoManager0);
    textField1.getDocument().addUndoableEditListener(undoManager1);

    JPanel p = new JPanel();
    p.add(new JButton(new AbstractAction("undo") {
      @Override public void actionPerformed(ActionEvent e) {
        if (undoManager0.canUndo()) {
          undoManager0.undo();
        }
        if (undoManager1.canUndo()) {
          undoManager1.undo();
        }
      }
    }));
    p.add(new JButton(new AbstractAction("redo") {
      @Override public void actionPerformed(ActionEvent e) {
        if (undoManager0.canRedo()) {
          undoManager0.redo();
        }
        if (undoManager1.canRedo()) {
          undoManager1.redo();
        }
      }
    }));
    p.add(new JButton(new AbstractAction("setText(new Date())") {
      @Override public void actionPerformed(ActionEvent e) {
        String str = new Date().toString();
        textField0.setText(str);
        textField1.setText(str);
      }
    }));

    Box box = Box.createVerticalBox();
    box.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    box.add(makePanel("Default", textField0));
    box.add(Box.createVerticalStrut(5));
    box.add(makePanel("replace ignoring undo", textField1));

    JPanel pp = new JPanel(new BorderLayout());
    pp.add(box, BorderLayout.NORTH);
    pp.add(p, BorderLayout.SOUTH);
    return pp;
  }
  private static JPanel makePanel(String title, JComponent c) {
    JPanel p = new JPanel(new BorderLayout());
    p.setBorder(BorderFactory.createTitledBorder(title));
    p.add(c);
    return p;
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
      @Override public void run() {
        createAndShowGUI();
      }
    });
  }
  public static void createAndShowGUI() {
    JFrame f = new JFrame();
    f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    f.getContentPane().add(new UndoManagerTest().makeUI());
    f.setSize(320, 240);
    f.setLocationRelativeTo(null);
    f.setVisible(true);
  }
}

class CustomUndoPlainDocument extends PlainDocument {
  private CompoundEdit compoundEdit;
  @Override protected void fireUndoableEditUpdate(UndoableEditEvent e) {
    if (compoundEdit == null) {
      super.fireUndoableEditUpdate(e);
    } else {
      compoundEdit.addEdit(e.getEdit());
    }
  }
  @Override public void replace(
      int offset, int length,
      String text, AttributeSet attrs) throws BadLocationException {
    if (length == 0) {
      System.out.println("insert");
      super.replace(offset, length, text, attrs);
    } else {
      System.out.println("replace");
      compoundEdit = new CompoundEdit();
      super.fireUndoableEditUpdate(new UndoableEditEvent(this, compoundEdit));
      super.replace(offset, length, text, attrs);
      compoundEdit.end();
      compoundEdit = null;
    }
  }
}
Dividend answered 27/6, 2014 at 11:27 Comment(1)
Right. The overridden replace() method fixes it.Lanthanide
S
0

I needed a solution that combined grouping the remove/insert of replace with a single undo (aterai's answer) plus treating continuous insertion/deletions of single characters as a single undo (similar to http://java-sl.com/tip_merge_undo_edits.html).

The combined code is:

   /*##################*/
   /* TextCompoundEdit */
   /*##################*/
   class TextCompoundEdit extends CompoundEdit
     {
      private boolean isUnDone = false;

      /*************/
      /* getLength */
      /*************/
      public int getLength()
        {
         return edits.size();
        }

      /********/
      /* undo */
      /********/
      public void undo() throws CannotUndoException
        {
         super.undo();
         isUnDone = true;
        }

      /********/
      /* redo */
      /********/
      public void redo() throws CannotUndoException
        {
         super.redo();
         isUnDone = false;
        }

      /***********/
      /* canUndo */
      /***********/
      public boolean canUndo()
        {
         return (edits.size() > 0) && (! isUnDone);
        }

      /***********/
      /* canRedo */
      /***********/
      public boolean canRedo()
        {
         return (edits.size() > 0) && isUnDone;
        }
     }

   /*#################*/
   /* TextUndoManager */
   /*#################*/
   class TextUndoManager extends AbstractUndoableEdit 
                         implements UndoableEditListener
     {     
      private String lastEditName = null;
      private int lastStart = 0;
      private ArrayList<TextCompoundEdit> edits = new ArrayList<TextCompoundEdit>();
      private TextCompoundEdit current;
      private int pointer = -1;
      private int groupIndex = 0;
      private String groupName = null;

      /************************/
      /* undoableEditHappened */
      /************************/
      public void undoableEditHappened(
        UndoableEditEvent e)
        {
         boolean isNeedStart = false;
         UndoableEdit edit = e.getEdit();

         if (! (edit instanceof AbstractDocument.DefaultDocumentEvent))
           { return; }

         AbstractDocument.DefaultDocumentEvent event = (AbstractDocument.DefaultDocumentEvent) edit;

         int start = event.getOffset();

         String editName;

         /*============================================*/
         /* If an explicit group name is not present,  */
         /* use the INSERT/REMOVE name from the event. */
         /*============================================*/

         if (groupName != null)
           { editName = groupName; }
         else
           { editName = event.getType().toString(); }

         /*============================*/
         /* Create a new compound edit */
         /* for the very first edit.   */
         /*============================*/

         if (current == null)
           { isNeedStart = true; }

         /*============================*/
         /* Create a new compound edit */
         /* for a different operation. */
         /*============================*/

         else if ((lastEditName == null) || 
                  (! lastEditName.equals(editName)))
           { isNeedStart = true; }

         /*================================================*/
         /* Only group continuous single character inserts */
         /* and deletes. Create a new edit if the user has */
         /* moved the caret from its prior position.       */
         /*================================================*/

         else if (groupName == null)
           {            
            if ((event.getType() == DocumentEvent.EventType.INSERT) &&
                     (start != (lastStart + 1)))
              { isNeedStart = true; }
            else if ((event.getType() == DocumentEvent.EventType.REMOVE) &&
                     (start != (lastStart - 1)))
              { isNeedStart = true; }
           }

         /*=========================================*/
         /* Adding a new edit will clear all of the */
         /* redos forward of the current position.  */
         /*=========================================*/

         while (pointer < edits.size() - 1)
           {
            edits.remove(edits.size() - 1);
            isNeedStart = true;
           }

         /*===================*/
         /* Add the new edit. */
         /*===================*/

         if (isNeedStart)
           { createCompoundEdit(); }

         current.addEdit(edit);

         /*=====================================*/
         /* Remember prior state for next edit. */
         /*=====================================*/

         lastEditName = editName;
         lastStart = start;
        }

      /*********************/
      /* startEditGrouping */
      /*********************/
      public void startEditGrouping()
        {
         groupName = "Group-" + groupIndex++;
        }

      /********************/
      /* stopEditGrouping */
      /********************/
      public void stopEditGrouping()
        {
         groupName = null;
        }

      /**********************/
      /* createCompoundEdit */
      /**********************/
      private void createCompoundEdit()
        {
         if (current == null)
           { current = new TextCompoundEdit(); }
         else if (current.getLength() > 0)
           { current = new TextCompoundEdit(); }

         edits.add(current);
         pointer++;
        }

      /********/
      /* undo */
      /********/ 
      public void undo() throws CannotUndoException
        {
         if (! canUndo())
           { throw new CannotUndoException(); }

         TextCompoundEdit u = edits.get(pointer);
         u.undo();
         pointer--;
        }

      /********/
      /* redo */
      /********/
      public void redo() throws CannotUndoException
        {
         if (! canRedo())
           { throw new CannotUndoException(); }

         pointer++;
         TextCompoundEdit u = edits.get(pointer);
         u.redo();
        }

      /***********/
      /* canUndo */
      /***********/
      public boolean canUndo()
        { 
         return pointer >= 0; 
        }

      /***********/
      /* canRedo */
      /***********/
      public boolean canRedo()
        {
         return (edits.size() > 0) && (pointer < (edits.size() - 1));
        }
     }

   /*#######################*/
   /* TextUndoPlainDocument */
   /*#######################*/
   class TextUndoPlainDocument extends PlainDocument 
     {    
      private TextUndoManager undoManager;

      /*************************/
      /* TextUndoPlainDocument */
      /*************************/
      TextUndoPlainDocument(
        TextUndoManager theManager)
        {
         super();
         undoManager = theManager;
         this.addUndoableEditListener(undoManager);
        }

      /***********/
      /* replace */
      /***********/
      @Override 
      public void replace(
        int offset, 
        int length,
        String text, 
        AttributeSet attrs) throws BadLocationException
        {
         if (length == 0)
           { super.replace(offset,length,text,attrs); }
         else
           {
            undoManager.startEditGrouping();
            super.replace(offset,length,text,attrs); 
            undoManager.stopEditGrouping();
           }
        }
     }

I invoke it in this way:

JTextArea textArea = new JTextArea(); 
TextUndoManager textAreaUndo = new TextUndoManager();
textArea.setDocument(new TextUndoPlainDocument(textAreaUndo));
Soloma answered 28/5, 2016 at 21:25 Comment(0)
D
0
    JTextArea jTextArea = new JTextArea("");
    UndoManager jTextAreaUndoManager = new UndoManager();
    jTextArea.getDocument().addUndoableEditListener(jTextAreaUndoManager);

    jTextArea.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {
                //ctrl+z
                if (e.getKeyChar() == 26) {
                    jTextAreaUndoManager.undo();
                }
                //ctrl+y
                if (e.getKeyChar() == 25) {
                    jTextAreaUndoManager.redo();
                }
            }
        });

Dante answered 10/5, 2020 at 15:38 Comment(1)
Some additional explanation or reference links would spice up your answer.Stercoricolous
B
0

If someone came here because they have the same problem, this is the simple solution I used (not very professional but it works):

if (undoManager.canUndo()) {
    undoManager.undo();
}

//repeat undo if textArea is empty
if (textArea.getText().isEmpty() && undoManager.canUndo()){
    undoManager.undo();
}

Do the same for redo.

Bubonocele answered 5/2 at 17:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.