I\'m using an UndoManager to capture changes in my JTextArea.
The method setText() however deletes everything and then pastes
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 edits = new ArrayList();
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));