Undo in JTextField and setText

2019-07-21 05:55发布

问题:

JTextField has an undo support out of box. It works fine for user interacion, but unfortunately if method setText(String str) is called, that results two UndoableEdits instead of one. So this code looks and feels fine but does not work:

UndoManager undoManager = new UndoManager();
JTextField tf = new JTextField();
tf.setText("initial value");
tf.getDocument().addUndoableEditListener(undoManager);
tf.setText("new value");
undoManager.undo();
System.out.println(tf.getText()); // Prints empty string
undoManager.undo();
System.out.println(tf.getText()); // Prints "initial value" as expected

Can JTextField somehow handle setText() as only one UndoableEdit?

回答1:

Another option is to override Document#replace(...)

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

public class ReplaceUndoableEditDemo {
  private final UndoManager um = new UndoManager();
  private final JTextField tf = new JTextField(24);
  private final UndoManager undoManager = new UndoManager();
  private final JTextField field = new JTextField(24);
  private final Document doc = new PlainDocument() {
    @Override public void replace(
      int offset, int length, String text, AttributeSet attrs)
    throws BadLocationException {
      undoManager.undoableEditHappened(new UndoableEditEvent(
          this, new ReplaceUndoableEdit(offset, length, text)));
      replaceIgnoringUndo(offset, length, text, attrs);
    }
    private void replaceIgnoringUndo(
      int offset, int length, String text, AttributeSet attrs)
    throws BadLocationException {
      for(UndoableEditListener uel: getUndoableEditListeners()) {
        removeUndoableEditListener(uel);
      }
      super.replace(offset, length, text, attrs);
      for(UndoableEditListener uel: getUndoableEditListeners()) {
        addUndoableEditListener(uel);
      }
    }
    class ReplaceUndoableEdit extends AbstractUndoableEdit {
      private final String oldValue;
      private final String newValue;
      private int offset;
      public ReplaceUndoableEdit(int offset, int length, String newValue) {
        String txt;
        try {
          txt = getText(offset, length);
        } catch(BadLocationException e) {
          txt = null;
        }
        this.oldValue = txt;
        this.newValue = newValue;
        this.offset = offset;
      }
      @Override public void undo() throws CannotUndoException {
        try {
          replaceIgnoringUndo(offset, newValue.length(), oldValue, null);
        } catch(BadLocationException ex) {
          throw new CannotUndoException();
        }
      }
      @Override public void redo() throws CannotRedoException {
        try {
          replaceIgnoringUndo(offset, oldValue.length(), newValue, null);
        } catch(BadLocationException ex) {
          throw new CannotUndoException();
        }
      }
      @Override public boolean canUndo() {
        return true;
      }
      @Override public boolean canRedo() {
        return true;
      }
    }
  };
  public JComponent makeUI() {
    tf.getDocument().addUndoableEditListener(um);
    doc.addUndoableEditListener(undoManager);
    field.setDocument(doc);
    field.setText("aaaaaaaaa");
    tf.setText("default");
    JPanel p = new JPanel();
    p.add(tf);
    p.add(field);
    p.add(new JButton(new AbstractAction("undo") {
      @Override public void actionPerformed(ActionEvent e) {
        try {
          undoManager.undo();
          um.undo();
        } catch(Exception ex) {
          java.awt.Toolkit.getDefaultToolkit().beep();
        }
      }
    }));
    p.add(new JButton(new AbstractAction("redo") {
      @Override public void actionPerformed(ActionEvent e) {
        try {
          undoManager.redo();
          um.redo();
        } catch(Exception ex) {
          java.awt.Toolkit.getDefaultToolkit().beep();
        }
      }
    }));
    p.add(new JButton(new AbstractAction("setText") {
      @Override public void actionPerformed(ActionEvent e) {
        String str = new Date().toString();
        tf.setText(str);
        field.setText(str);
      }
    }));
    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 ReplaceUndoableEditDemo().makeUI());
    f.setSize(320, 240);
    f.setLocationRelativeTo(null);
    f.setVisible(true);
  }
}


回答2:

not fans of DragAndDrop and UndoAndRedo

have to load data to the UndoManager and define UndoAction undoAction = new UndoAction(); the same is possible for simple Graphics or e.i.

   class UndoHandler implements UndoableEditListener {

        @Override
        public void undoableEditHappened(UndoableEditEvent e) {
            undoManager.addEdit(e.getEdit());
            undoAction.update();
        }
    }

and to create Swing Action (add to JButton) for flushing contens back to the JTextField

    class UndoAction extends AbstractAction {

        private static final long serialVersionUID = 1L;

        UndoAction() {
            super("Undo");
            setEnabled(false);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                undo.undo();
            } catch (CannotUndoException ex) {
                System.out.println("Unable to undo: " + ex);
                ex.printStackTrace();
            }
            update();
        }

        protected void update() {
            if (undo.canUndo()) {
                setEnabled(true);
                putValue(Action.NAME, undo.getUndoPresentationName());
            } else {
                setEnabled(false);
                putValue(Action.NAME, "Undo");
            }
        }
   } 


回答3:

Found a workaround:

public class SetTextEditUndo extends AbstractUndoableEdit {

    public JTextField src;
    public String oldValue;
    public String newValue;

    public SetTextEditUndo(JTextField src, String oldValue, String newValue) {
        this.src = src;
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

    @Override
    public void undo() throws CannotUndoException {
        setTextIgnoringUndo(src, oldValue);
    }

    @Override
    public void redo() throws CannotRedoException {
        setTextIgnoringUndo(src, newValue);
    }

    @Override
    public boolean canUndo() {
        return true;
    }

    @Override
    public boolean canRedo() {
        return true;
    }

    public static void setTextIgnoringUndo(JTextField tf, String str) {
        PlainDocument doc = (PlainDocument) tf.getDocument();
        UndoableEditListener uel = doc.getUndoableEditListeners()[0];
        doc.removeUndoableEditListener(uel);
        tf.setText(str);
        doc.addUndoableEditListener(uel);
    }

}