/* ******************************************************************************
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.ui.richtext;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.AbstractOperation;
import org.eclipse.core.commands.operations.IContextReplacingOperation;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.ObjectUndoContext;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.commands.operations.OperationHistoryFactory;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.text.undo.IDocumentUndoListener;
/**
* @author Frank Shaka
*/
public class RichDocumentUndoManager implements IRichDocumentUndoManager {
/**
* Represents an undo-able text change, described as the replacement of some
* preserved text with new text.
* <p>
* Based on the DefaultUndoManager.TextCommand from R3.1.
* </p>
*/
private static class UndoableRichTextChange extends AbstractOperation {
/** The start index of the replaced text. */
protected int fStart = -1;
/** The end index of the replaced text. */
protected int fEnd = -1;
/** The newly inserted text. */
protected String fText;
/** The replaced text. */
protected String fPreservedText;
/** The undo modification stamp. */
protected long fUndoModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/** The redo modification stamp. */
protected long fRedoModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/** The undo manager that generated the change. */
protected RichDocumentUndoManager fDocumentUndoManager;
protected StyleRange[] oldTextStyles;
protected StyleRange[] newTextStyles;
protected LineStyle[] oldLineStyles;
protected LineStyle[] newLineStyles;
protected ImagePlaceHolder[] oldImages;
protected ImagePlaceHolder[] newImages;
protected Hyperlink[] oldHyperlinks;
protected Hyperlink[] newHyperlinks;
/**
* Creates a new text change.
*
* @param manager
* the undo manager for this change
*/
UndoableRichTextChange(RichDocumentUndoManager manager) {
super(""); //$NON-NLS-1$
this.fDocumentUndoManager = manager;
addContext(manager.getUndoContext());
}
/**
* Re-initializes this text change.
*/
protected void reinitialize() {
reinitializeTextChange();
oldTextStyles = null;
newTextStyles = null;
oldLineStyles = null;
newLineStyles = null;
oldImages = null;
newImages = null;
oldHyperlinks = null;
newHyperlinks = null;
}
protected void reinitializeTextChange() {
fStart = fEnd = -1;
fText = fPreservedText = null;
fUndoModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
fRedoModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
}
/**
* Sets the start and the end index of this change.
*
* @param start
* the start index
* @param end
* the end index
*/
protected void set(int start, int end) {
fStart = start;
fEnd = end;
fText = null;
fPreservedText = null;
}
/*
* @see
* org.eclipse.core.commands.operations.IUndoableOperation#dispose()
*/
public void dispose() {
reinitialize();
}
/**
* Undo the change described by this change.
*/
protected void undoTextChange() {
if (fStart < 0 || fEnd < 0)
return;
try {
if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4)
((IDocumentExtension4) fDocumentUndoManager.fDocument)
.replace(fStart, fText.length(), fPreservedText,
fUndoModificationStamp);
else
fDocumentUndoManager.fDocument.replace(fStart, fText
.length(), fPreservedText);
} catch (BadLocationException x) {
}
}
protected void undoRichTextChange() {
// fDocumentUndoManager.ignoreDocumentChange = true;
if (oldTextStyles != null)
fDocumentUndoManager.fDocument.setTextStyles(oldTextStyles);
if (oldLineStyles != null)
fDocumentUndoManager.fDocument.setLineStyles(oldLineStyles);
if (oldImages != null)
fDocumentUndoManager.fDocument.setImages(oldImages);
if (oldHyperlinks != null)
fDocumentUndoManager.fDocument.setHyperlinks(oldHyperlinks);
// fDocumentUndoManager.ignoreDocumentChange = false;
}
/*
* @see
* org.eclipse.core.commands.operations.IUndoableOperation#canUndo()
*/
public boolean canUndo() {
if (isValid()) {
if (fStart >= 0 && fEnd >= 0 && fText != null) {
if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4) {
long docStamp = ((IDocumentExtension4) fDocumentUndoManager.fDocument)
.getModificationStamp();
// Normal case: an undo is valid if its redo will restore
// document to its current modification stamp
boolean canUndo = docStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP
|| docStamp == getRedoModificationStamp();
/*
* Special case to check if the answer is false. If the
* last document change was empty, then the document's
* modification stamp was incremented but nothing was
* committed. The operation being queried has an older
* stamp. In this case only, the comparison is
* different. A sequence of document changes that
* include an empty change is handled correctly when a
* valid commit follows the empty change, but when
* #canUndo() is queried just after an empty change, we
* must special case the check. The check is very
* specific to prevent false positives. see
* https://bugs.eclipse.org/bugs/show_bug.cgi?id=98245
*/
if (!canUndo
&& this == fDocumentUndoManager.fHistory
.getUndoOperation(fDocumentUndoManager.fUndoContext)
// this is the latest operation
&& this != fDocumentUndoManager.fCurrent
// there is a more current operation not on the stack
&& !fDocumentUndoManager.fCurrent.isValid()
// the current operation is not a valid document
// modification
&& fDocumentUndoManager.fCurrent.fUndoModificationStamp !=
// the invalid current operation has a document stamp
IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) {
canUndo = fDocumentUndoManager.fCurrent.fRedoModificationStamp == docStamp;
}
/*
* When the composite is the current operation, it may
* hold the timestamp of a no-op change. We check this
* here rather than in an override of canUndo() in
* UndoableCompoundTextChange simply to keep all the
* special case checks in one place.
*/
if (!canUndo
&& this == fDocumentUndoManager.fHistory
.getUndoOperation(fDocumentUndoManager.fUndoContext)
&& // this is the latest operation
this instanceof UndoableCompoundRichTextChange
&& this == fDocumentUndoManager.fCurrent && // this is the current operation
this.fStart == -1 && // the current operation text is not valid
fDocumentUndoManager.fCurrent.fRedoModificationStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) {
// but it has a redo stamp
canUndo = fDocumentUndoManager.fCurrent.fRedoModificationStamp == docStamp;
}
}
}
// if there is no timestamp to check, simply return true per the
// 3.0.1 behavior
return true;
}
return false;
}
/*
* @see
* org.eclipse.core.commands.operations.IUndoableOperation#canRedo()
*/
public boolean canRedo() {
if (isValid()) {
if (fStart >= 0 && fEnd >= 0 && fText != null) {
if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4) {
long docStamp = ((IDocumentExtension4) fDocumentUndoManager.fDocument)
.getModificationStamp();
return docStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP
|| docStamp == getUndoModificationStamp();
}
}
// if there is no timestamp to check, simply return true per the
// 3.0.1 behavior
return true;
}
return false;
}
/*
* @see
* org.eclipse.core.commands.operations.IUndoableOperation#canExecute()
*/
public boolean canExecute() {
return fDocumentUndoManager.isConnected();
}
/*
* @seeorg.eclipse.core.commands.operations.IUndoableOperation.
* IUndoableOperation#execute(IProgressMonitor, IAdaptable)
*/
public IStatus execute(IProgressMonitor monitor, IAdaptable uiInfo) {
// Text changes execute as they are typed, so executing one has no
// effect.
return Status.OK_STATUS;
}
/**
* {@inheritDoc} Notifies clients about the undo.
*/
public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) {
if (isValid()) {
fDocumentUndoManager.fireDocumentUndo(fStart, fPreservedText,
fText, uiInfo, RichDocumentUndoEvent.ABOUT_TO_UNDO,
false);
undoTextChange();
undoRichTextChange();
fDocumentUndoManager.resetProcessChangeState();
fDocumentUndoManager.fireDocumentUndo(fStart, fPreservedText,
fText, uiInfo, RichDocumentUndoEvent.UNDONE, false);
return Status.OK_STATUS;
}
return IOperationHistory.OPERATION_INVALID_STATUS;
}
/**
* Re-applies the change described by this change.
*/
protected void redoTextChange() {
if (fStart < 0 || fEnd < 0)
return;
try {
if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4)
((IDocumentExtension4) fDocumentUndoManager.fDocument)
.replace(fStart, fEnd - fStart, fText,
fRedoModificationStamp);
else
fDocumentUndoManager.fDocument.replace(fStart, fEnd
- fStart, fText);
} catch (BadLocationException x) {
}
}
protected void redoRichTextChange() {
// fDocumentUndoManager.ignoreDocumentChange = true;
if (newTextStyles != null)
fDocumentUndoManager.fDocument.setTextStyles(newTextStyles);
if (newLineStyles != null)
fDocumentUndoManager.fDocument.setLineStyles(newLineStyles);
if (newImages != null)
fDocumentUndoManager.fDocument.setImages(newImages);
if (newHyperlinks != null)
fDocumentUndoManager.fDocument.setHyperlinks(newHyperlinks);
// fDocumentUndoManager.ignoreDocumentChange = false;
}
/**
* Re-applies the change described by this change that was previously
* undone. Also notifies clients about the redo.
*
* @param monitor
* the progress monitor to use if necessary
* @param uiInfo
* an adaptable that can provide UI info if needed
* @return the status
*/
public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) {
if (isValid()) {
redoTextChange();
redoRichTextChange();
fDocumentUndoManager.resetProcessChangeState();
fDocumentUndoManager.fireDocumentUndo(fStart, fText,
fPreservedText, uiInfo, RichDocumentUndoEvent.REDONE,
false);
return Status.OK_STATUS;
}
return IOperationHistory.OPERATION_INVALID_STATUS;
}
/**
* Update the change in response to a commit.
*/
protected void updateTextChange() {
fText = fDocumentUndoManager.fTextBuffer.toString();
fDocumentUndoManager.fTextBuffer.setLength(0);
fPreservedText = fDocumentUndoManager.fPreservedTextBuffer
.toString();
fDocumentUndoManager.fPreservedTextBuffer.setLength(0);
}
/**
* Creates a new uncommitted text change depending on whether a compound
* change is currently being executed.
*
* @return a new, uncommitted text change or a compound text change
*/
protected UndoableRichTextChange createCurrent() {
if (fDocumentUndoManager.fFoldingIntoCompoundChange)
return new UndoableCompoundRichTextChange(fDocumentUndoManager);
return new UndoableRichTextChange(fDocumentUndoManager);
}
/**
* Commits the current change into this one.
*/
protected void commit() {
if (!isValid()) {//fStart < 0 ) {
if (fDocumentUndoManager.fFoldingIntoCompoundChange) {
fDocumentUndoManager.fCurrent = createCurrent();
} else {
reinitialize();
}
} else {
updateTextChange();
fDocumentUndoManager.fCurrent = createCurrent();
}
fDocumentUndoManager.resetProcessChangeState();
}
/**
* Updates the text from the buffers without resetting the buffers or
* adding anything to the stack.
*/
protected void pretendCommit() {
if (fStart > -1) {
fText = fDocumentUndoManager.fTextBuffer.toString();
fPreservedText = fDocumentUndoManager.fPreservedTextBuffer
.toString();
}
}
/**
* Attempt a commit of this change and answer true if a new fCurrent was
* created as a result of the commit.
*
* @return <code>true</code> if the change was committed and created a
* new <code>fCurrent</code>, <code>false</code> if not
*/
protected boolean attemptCommit() {
pretendCommit();
if (isValid()) {
fDocumentUndoManager.commit();
return true;
}
return false;
}
/**
* Checks whether this text change is valid for undo or redo.
*
* @return <code>true</code> if the change is valid for undo or redo
*/
protected boolean isValid() {
return (fStart > -1 && fEnd > -1 && fText != null)
|| (oldImages != null && newImages != null)
|| (oldLineStyles != null && newLineStyles != null)
|| (oldTextStyles != null && newTextStyles != null)
|| (oldHyperlinks != null && newHyperlinks != null);
}
/*
* @see java.lang.Object#toString()
*/
public String toString() {
String delimiter = ", "; //$NON-NLS-1$
StringBuffer text = new StringBuffer(super.toString());
text.append("\n"); //$NON-NLS-1$
text.append(this.getClass().getName());
text.append(" undo modification stamp: "); //$NON-NLS-1$
text.append(fUndoModificationStamp);
text.append(" redo modification stamp: "); //$NON-NLS-1$
text.append(fRedoModificationStamp);
text.append(" start: "); //$NON-NLS-1$
text.append(fStart);
text.append(delimiter);
text.append("end: "); //$NON-NLS-1$
text.append(fEnd);
text.append(delimiter);
text.append("text: '"); //$NON-NLS-1$
text.append(fText);
text.append('\'');
text.append(delimiter);
text.append("preservedText: '"); //$NON-NLS-1$
text.append(fPreservedText);
text.append('\'');
return text.toString();
}
/**
* Return the undo modification stamp
*
* @return the undo modification stamp for this change
*/
protected long getUndoModificationStamp() {
return fUndoModificationStamp;
}
/**
* Return the redo modification stamp
*
* @return the redo modification stamp for this change
*/
protected long getRedoModificationStamp() {
return fRedoModificationStamp;
}
}
/**
* Represents an undo-able text change consisting of several individual
* changes.
*/
private static class UndoableCompoundRichTextChange extends
UndoableRichTextChange {
/** The list of individual changes */
private List<UndoableRichTextChange> fChanges = new ArrayList<UndoableRichTextChange>();
/**
* Creates a new compound text change.
*
* @param manager
* the undo manager for this change
*/
UndoableCompoundRichTextChange(RichDocumentUndoManager manager) {
super(manager);
}
/**
* Adds a new individual change to this compound change.
*
* @param change
* the change to be added
*/
protected void add(UndoableRichTextChange change) {
fChanges.add(change);
}
/*
* @see
* org.eclipse.text.undo.UndoableTextChange#undo(org.eclipse.core.runtime
* .IProgressMonitor, org.eclipse.core.runtime.IAdaptable)
*/
public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) {
int size = fChanges.size();
if (size > 0) {
UndoableRichTextChange c;
c = (UndoableRichTextChange) fChanges.get(0);
fDocumentUndoManager.fireDocumentUndo(c.fStart,
c.fPreservedText, c.fText, uiInfo,
RichDocumentUndoEvent.ABOUT_TO_UNDO, true);
for (int i = size - 1; i >= 0; --i) {
c = (UndoableRichTextChange) fChanges.get(i);
c.undoTextChange();
}
fDocumentUndoManager.resetProcessChangeState();
fDocumentUndoManager.fireDocumentUndo(c.fStart,
c.fPreservedText, c.fText, uiInfo,
RichDocumentUndoEvent.UNDONE, true);
}
undoRichTextChange();
return Status.OK_STATUS;
}
/*
* @see
* org.eclipse.text.undo.UndoableTextChange#redo(org.eclipse.core.runtime
* .IProgressMonitor, org.eclipse.core.runtime.IAdaptable)
*/
public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) {
int size = fChanges.size();
if (size > 0) {
UndoableRichTextChange c;
c = (UndoableRichTextChange) fChanges.get(size - 1);
fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fText,
c.fPreservedText, uiInfo,
RichDocumentUndoEvent.ABOUT_TO_REDO, true);
for (int i = 0; i <= size - 1; ++i) {
c = (UndoableRichTextChange) fChanges.get(i);
c.redoTextChange();
}
fDocumentUndoManager.resetProcessChangeState();
fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fText,
c.fPreservedText, uiInfo, RichDocumentUndoEvent.REDONE,
true);
}
redoRichTextChange();
return Status.OK_STATUS;
}
/*
* @see org.eclipse.text.undo.UndoableTextChange#updateTextChange()
*/
protected void updateTextChange() {
// first gather the data from the buffers
super.updateTextChange();
// the result of the update is stored as a child change
UndoableRichTextChange c = new UndoableRichTextChange(
fDocumentUndoManager);
c.fStart = fStart;
c.fEnd = fEnd;
c.fText = fText;
c.fPreservedText = fPreservedText;
c.fUndoModificationStamp = fUndoModificationStamp;
c.fRedoModificationStamp = fRedoModificationStamp;
// c.oldImages = oldImages;
// c.newImages = newImages;
// c.oldLineStyles = oldLineStyles;
// c.newLineStyles = newLineStyles;
// c.oldTextStyles = oldTextStyles;
// c.newTextStyles = newTextStyles;
add(c);
// clear out all indexes now that the child is added
reinitializeTextChange();
}
/*
* @see org.eclipse.text.undo.UndoableTextChange#createCurrent()
*/
protected UndoableRichTextChange createCurrent() {
if (!fDocumentUndoManager.fFoldingIntoCompoundChange)
return new UndoableRichTextChange(fDocumentUndoManager);
reinitialize();
return this;
}
/*
* @see org.eclipse.text.undo.UndoableTextChange#commit()
*/
protected void commit() {
// if there is pending data, update the text change
if (fStart > -1)
updateTextChange();
fDocumentUndoManager.fCurrent = createCurrent();
fDocumentUndoManager.resetProcessChangeState();
}
/*
* @see org.eclipse.text.undo.UndoableTextChange#isValid()
*/
protected boolean isValid() {
return fStart > -1 || fChanges.size() > 0
|| (oldImages != null && newImages != null)
|| (oldLineStyles != null && newLineStyles != null)
|| (oldTextStyles != null && newTextStyles != null)
|| (oldHyperlinks != null && newHyperlinks != null);
}
/*
* @see
* org.eclipse.text.undo.UndoableTextChange#getUndoModificationStamp()
*/
protected long getUndoModificationStamp() {
if (fStart > -1)
return super.getUndoModificationStamp();
else if (fChanges.size() > 0)
return ((UndoableRichTextChange) fChanges.get(0))
.getUndoModificationStamp();
return fUndoModificationStamp;
}
/*
* @see
* org.eclipse.text.undo.UndoableTextChange#getRedoModificationStamp()
*/
protected long getRedoModificationStamp() {
if (fStart > -1)
return super.getRedoModificationStamp();
else if (fChanges.size() > 0)
return ((UndoableRichTextChange) fChanges
.get(fChanges.size() - 1)).getRedoModificationStamp();
return fRedoModificationStamp;
}
}
private class RichDocumentListener implements IRichDocumentListener {
public void imageChanged(IRichDocument document,
ImagePlaceHolder[] oldImages, ImagePlaceHolder[] newImages) {
handleRichDocumentEvent(null, null, null, null, oldImages,
newImages, null, null);
}
public void lineStyleChanged(IRichDocument document,
LineStyle[] oldLineStyles, LineStyle[] newLineStyles) {
handleRichDocumentEvent(null, null, oldLineStyles, newLineStyles,
null, null, null, null);
}
public void textStyleChanged(IRichDocument document,
StyleRange[] oldTextStyles, StyleRange[] newTextStyles) {
handleRichDocumentEvent(oldTextStyles, newTextStyles, null, null,
null, null, null, null);
}
public void hyperlinkChanged(IRichDocument document,
Hyperlink[] oldHyperlinks, Hyperlink[] newHyperlinks) {
handleRichDocumentEvent(null, null, null, null, null, null,
oldHyperlinks, newHyperlinks);
}
private void handleRichDocumentEvent(StyleRange[] oldStyles,
StyleRange[] newStyles, LineStyle[] oldLineStyles,
LineStyle[] newLineStyle, ImagePlaceHolder[] oldImages,
ImagePlaceHolder[] newImages, Hyperlink[] oldHyperlinks,
Hyperlink[] newHyperlinks) {
if (ignoreDocumentChange)
return;
IUndoableOperation op = fHistory.getUndoOperation(fUndoContext);
boolean wasValid = false;
if (op != null)
wasValid = op.canUndo();
// if ( !foldingRichTextChange ) {
// fCurrent.attemptCommit();
// }
processRichChange(fCurrent, oldStyles, newStyles, oldLineStyles,
newLineStyle, oldImages, newImages, oldHyperlinks,
newHyperlinks);
if (op == fCurrent) {
// if the document change did not cause a new fCurrent to be
// created, then we should
// notify the history that the current operation changed if its
// validity has changed.
if (wasValid != fCurrent.isValid()) {
fHistory.operationChanged(op);
} else {
}
} else {
// if the change created a new fCurrent that we did not yet add
// to the
// stack, do so if it's valid and we are not in the middle of a
// compound change.
if (fCurrent != fLastAddedTextEdit && fCurrent.isValid()) {
addToOperationHistory(fCurrent);
}
}
}
}
private void processRichChange(UndoableRichTextChange edit,
StyleRange[] oldTextStyles, StyleRange[] newTextStyle,
LineStyle[] oldLineStyles, LineStyle[] newLineStyle,
ImagePlaceHolder[] oldImages, ImagePlaceHolder[] newImages,
Hyperlink[] oldHyperlinks, Hyperlink[] newHyperlinks) {
if (oldTextStyles != null && edit.oldTextStyles == null)
edit.oldTextStyles = oldTextStyles;
if (newTextStyle != null)
edit.newTextStyles = newTextStyle;
if (oldLineStyles != null && edit.oldLineStyles == null)
edit.oldLineStyles = oldLineStyles;
if (newLineStyle != null)
edit.newLineStyles = newLineStyle;
if (oldImages != null && edit.oldImages == null)
edit.oldImages = oldImages;
if (newImages != null)
edit.newImages = newImages;
if (oldHyperlinks != null && edit.oldHyperlinks == null)
edit.oldHyperlinks = oldHyperlinks;
if (newHyperlinks != null)
edit.newHyperlinks = newHyperlinks;
}
/**
* Internal listener to document changes.
*/
private class DocumentListener implements IDocumentListener {
private String fReplacedText;
/*
* @see
* org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged
* (org.eclipse.jface.text.DocumentEvent)
*/
public void documentAboutToBeChanged(DocumentEvent event) {
if (ignoreDocumentChange)
return;
try {
fReplacedText = event.getDocument().get(event.getOffset(),
event.getLength());
fPreservedUndoModificationStamp = event.getModificationStamp();
} catch (BadLocationException x) {
fReplacedText = null;
}
}
/*
* @see
* org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse
* .jface.text.DocumentEvent)
*/
public void documentChanged(DocumentEvent event) {
if (ignoreDocumentChange)
return;
fPreservedRedoModificationStamp = event.getModificationStamp();
// record the current valid state for the top operation in case it
// remains the
// top operation but changes state.
IUndoableOperation op = fHistory.getUndoOperation(fUndoContext);
boolean wasValid = false;
if (op != null)
wasValid = op.canUndo();
// Process the change, providing the before and after timestamps
processChange(event.getOffset(), event.getOffset()
+ event.getLength(), event.getText(), fReplacedText,
fPreservedUndoModificationStamp,
fPreservedRedoModificationStamp);
// now update fCurrent with the latest buffers from the document
// change.
fCurrent.pretendCommit();
if (op == fCurrent) {
// if the document change did not cause a new fCurrent to be
// created, then we should
// notify the history that the current operation changed if its
// validity has changed.
if (wasValid != fCurrent.isValid())
fHistory.operationChanged(op);
} else {
// if the change created a new fCurrent that we did not yet add
// to the
// stack, do so if it's valid and we are not in the middle of a
// compound change.
if (fCurrent != fLastAddedTextEdit && fCurrent.isValid()) {
addToOperationHistory(fCurrent);
}
}
}
}
/*
* @see IOperationHistoryListener
*/
private class HistoryListener implements IOperationHistoryListener {
private IUndoableOperation fOperation;
public void historyNotification(final OperationHistoryEvent event) {
final int type = event.getEventType();
switch (type) {
case OperationHistoryEvent.ABOUT_TO_UNDO:
case OperationHistoryEvent.ABOUT_TO_REDO:
// if this is one of our operations
if (event.getOperation().hasContext(fUndoContext)) {
// if we are undoing/redoing an operation we generated, then
// ignore
// the document changes associated with this undo or redo.
if (event.getOperation() instanceof UndoableRichTextChange) {
// listenToTextChanges( false );
ignoreDocumentChange = true;
// in the undo case only, make sure compounds are closed
if (type == OperationHistoryEvent.ABOUT_TO_UNDO) {
if (fFoldingIntoCompoundChange) {
endCompoundChange();
}
}
} else {
// the undo or redo has our context, but it is not one
// of our edits. We will listen to the changes, but will
// reset the state that tracks the undo/redo history.
commit();
fLastAddedTextEdit = null;
}
fOperation = event.getOperation();
}
break;
case OperationHistoryEvent.UNDONE:
case OperationHistoryEvent.REDONE:
case OperationHistoryEvent.OPERATION_NOT_OK:
if (event.getOperation() == fOperation) {
// listenToTextChanges( true );
ignoreDocumentChange = false;
fOperation = null;
}
break;
}
}
}
/**
* The undo context for this document undo manager.
*/
private ObjectUndoContext fUndoContext;
/**
* The document whose changes are being tracked.
*/
private IRichDocument fDocument;
/**
* The currently constructed edit.
*/
private UndoableRichTextChange fCurrent;
/**
* The internal document listener.
*/
private DocumentListener fDocumentListener;
private RichDocumentListener fRichDocumentListener;
/**
* Indicates whether the current change belongs to a compound change.
*/
private boolean fFoldingIntoCompoundChange = false;
/**
* The operation history being used to store the undo history.
*/
private IOperationHistory fHistory;
/**
* The operation history listener used for managing undo and redo before and
* after the individual edits are performed.
*/
private IOperationHistoryListener fHistoryListener;
/**
* The text edit last added to the operation history. This must be tracked
* internally instead of asking the history, since outside parties may be
* placing items on our undo/redo history.
*/
private UndoableRichTextChange fLastAddedTextEdit = null;
/**
* The document modification stamp for redo.
*/
private long fPreservedRedoModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/**
* Text buffer to collect viewer content which has been replaced
*/
private StringBuffer fPreservedTextBuffer;
/**
* The document modification stamp for undo.
*/
private long fPreservedUndoModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/**
* The last delete text edit.
*/
private UndoableRichTextChange fPreviousDelete;
/**
* Text buffer to collect text which is inserted into the viewer
*/
private StringBuffer fTextBuffer;
/** Indicates inserting state. */
private boolean fInserting = false;
/** Indicates overwriting state. */
private boolean fOverwriting = false;
/** The registered document listeners. */
private ListenerList fDocumentUndoListeners;
/** The list of clients connected. */
private List<Object> fConnected;
private boolean ignoreDocumentChange = false;
// private boolean foldingRichTextChange = false;
//
// public void beginCompoundRichTextChange() {
// if ( isConnected() ) {
// this.foldingRichTextChange = true;
// }
// }
//
// public void endCompoundRichTextChange() {
// if ( isConnected() ) {
// this.foldingRichTextChange = false;
// }
// }
/**
* Create a DocumentUndoManager for the given document.
*
* @param document
* the document whose undo history is being managed.
*/
public RichDocumentUndoManager(IRichDocument document) {
super();
Assert.isNotNull(document);
fDocument = document;
fHistory = OperationHistoryFactory.getOperationHistory();
fUndoContext = new ObjectUndoContext(fDocument);
fConnected = new ArrayList<Object>();
fDocumentUndoListeners = new ListenerList();
}
/*
* @see
* org.eclipse.jface.text.IDocumentUndoManager#addDocumentUndoListener(org
* .eclipse.jface.text.IDocumentUndoListener)
*/
public void addDocumentUndoListener(IRichDocumentUndoListener listener) {
fDocumentUndoListeners.add(listener);
}
/*
* @see
* org.eclipse.jface.text.IDocumentUndoManager#removeDocumentUndoListener
* (org.eclipse.jface.text.IDocumentUndoListener)
*/
public void removeDocumentUndoListener(IRichDocumentUndoListener listener) {
fDocumentUndoListeners.remove(listener);
}
/*
* @see org.eclipse.jface.text.IDocumentUndoManager#getUndoContext()
*/
public IUndoContext getUndoContext() {
return fUndoContext;
}
/*
* @see org.eclipse.jface.text.IDocumentUndoManager#commit()
*/
public void commit() {
// if fCurrent has never been placed on the history, do so now.
// this can happen when there are multiple programmatically commits in a
// single document change.
if (fLastAddedTextEdit != fCurrent) {
fCurrent.pretendCommit();
if (fCurrent.isValid())
addToOperationHistory(fCurrent);
}
fCurrent.commit();
}
/*
* @see org.eclipse.text.undo.IDocumentUndoManager#reset()
*/
public void reset() {
if (isConnected()) {
shutdown();
initialize();
}
}
/*
* @see org.eclipse.text.undo.IDocumentUndoManager#redoable()
*/
public boolean redoable() {
return OperationHistoryFactory.getOperationHistory().canRedo(
fUndoContext);
}
/*
* @see org.eclipse.text.undo.IDocumentUndoManager#undoable()
*/
public boolean undoable() {
return OperationHistoryFactory.getOperationHistory().canUndo(
fUndoContext);
}
/*
* @see org.eclipse.text.undo.IDocumentUndoManager#undo()
*/
public void redo() throws ExecutionException {
if (isConnected() && redoable())
OperationHistoryFactory.getOperationHistory().redo(
getUndoContext(), null, null);
}
/*
* @see org.eclipse.text.undo.IDocumentUndoManager#undo()
*/
public void undo() throws ExecutionException {
if (undoable())
OperationHistoryFactory.getOperationHistory().undo(fUndoContext,
null, null);
}
/*
* @see
* org.eclipse.jface.text.IDocumentUndoManager#connect(java.lang.Object)
*/
public void connect(Object client) {
if (!isConnected()) {
initialize();
}
if (!fConnected.contains(client))
fConnected.add(client);
}
/*
* @see
* org.eclipse.jface.text.IDocumentUndoManager#disconnect(java.lang.Object)
*/
public void disconnect(Object client) {
fConnected.remove(client);
if (!isConnected()) {
shutdown();
}
}
/*
* @see org.eclipse.jface.text.IDocumentUndoManager#beginCompoundChange()
*/
public void beginCompoundChange() {
if (isConnected()) {
fFoldingIntoCompoundChange = true;
commit();
}
}
/*
* @see org.eclipse.jface.text.IDocumentUndoManager#endCompoundChange()
*/
public void endCompoundChange() {
if (isConnected()) {
fFoldingIntoCompoundChange = false;
commit();
}
}
/*
* @see org.eclipse.jface.text.IDocumentUndoManager#setUndoLimit(int)
*/
public void setMaximalUndoLevel(int undoLimit) {
fHistory.setLimit(fUndoContext, undoLimit);
}
/**
* Fires a document undo event to all registered document undo listeners.
* Uses a robust iterator.
*
* @param offset
* the document offset
* @param text
* the text that was inserted
* @param preservedText
* the text being replaced
* @param source
* the source which triggered the event
* @param eventType
* the type of event causing the change
* @param isCompound
* a flag indicating whether the change is a compound change
* @see IDocumentUndoListener
*/
void fireDocumentUndo(int offset, String text, String preservedText,
Object source, int eventType, boolean isCompound) {
if (offset < 0)
return;
eventType = isCompound ? eventType | RichDocumentUndoEvent.COMPOUND
: eventType;
RichDocumentUndoEvent event = new RichDocumentUndoEvent(fDocument,
offset, text, preservedText, eventType, source);
Object[] listeners = fDocumentUndoListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
((IRichDocumentUndoListener) listeners[i])
.documentUndoNotification(event);
}
}
/**
* Adds any listeners needed to track the document and the operations
* history.
*/
private void addListeners() {
fHistoryListener = new HistoryListener();
fHistory.addOperationHistoryListener(fHistoryListener);
// listenToTextChanges( true );
if (fDocumentListener == null && fDocument != null) {
fDocumentListener = new DocumentListener();
fDocument.addDocumentListener(fDocumentListener);
}
if (fRichDocumentListener == null && fDocument != null) {
fRichDocumentListener = new RichDocumentListener();
fDocument.addRichDocumentListener(fRichDocumentListener);
}
}
/**
* Removes any listeners that were installed by the document.
*/
private void removeListeners() {
if (fRichDocumentListener != null && fDocument != null) {
fDocument.removeRichDocumentListener(fRichDocumentListener);
fRichDocumentListener = null;
}
if (fDocumentListener != null && fDocument != null) {
fDocument.removeDocumentListener(fDocumentListener);
fDocumentListener = null;
}
// listenToTextChanges( false );
fHistory.removeOperationHistoryListener(fHistoryListener);
fHistoryListener = null;
}
/**
* Adds the given text edit to the operation history if it is not part of a
* compound change.
*
* @param edit
* the edit to be added
*/
private void addToOperationHistory(UndoableRichTextChange edit) {
if (!fFoldingIntoCompoundChange
|| edit instanceof UndoableCompoundRichTextChange) {
fHistory.add(edit);
fLastAddedTextEdit = edit;
}
}
/**
* Disposes the undo history.
*/
private void disposeUndoHistory() {
fHistory.dispose(fUndoContext, true, true, true);
}
/**
* Initializes the undo history.
*/
private void initializeUndoHistory() {
if (fHistory != null && fUndoContext != null)
fHistory.dispose(fUndoContext, true, true, false);
}
/**
* Checks whether the given text starts with a line delimiter and
* subsequently contains a white space only.
*
* @param text
* the text to check
* @return <code>true</code> if the text is a line delimiter followed by
* whitespace, <code>false</code> otherwise
*/
private boolean isWhitespaceText(String text) {
if (text == null || text.length() == 0)
return false;
String[] delimiters = fDocument.getLegalLineDelimiters();
int index = TextUtilities.startsWith(delimiters, text);
if (index > -1) {
char c;
int length = text.length();
for (int i = delimiters[index].length(); i < length; i++) {
c = text.charAt(i);
if (c != ' ' && c != '\t')
return false;
}
return true;
}
return false;
}
// /**
// * Switches the state of whether there is a text listener or not.
// *
// * @param listen the state which should be established
// */
// private void listenToTextChanges( boolean listen ) {
// if ( listen ) {
// if ( fDocumentListener == null && fDocument != null ) {
// fDocumentListener = new DocumentListener();
// fDocument.addDocumentListener( fDocumentListener );
// }
// if ( fRichDocumentListener == null && fDocument != null ) {
// fRichDocumentListener = new RichDocumentListener();
// fDocument.addRichDocumentListener( fRichDocumentListener );
// }
// }else if ( !listen ) {
// if ( fRichDocumentListener != null && fDocument != null ) {
// fDocument.removeRichDocumentListener( fRichDocumentListener );
// fRichDocumentListener = null;
// }
// if ( fDocumentListener != null && fDocument != null ) {
// fDocument.removeDocumentListener( fDocumentListener );
// fDocumentListener = null;
// }
// }
// }
private void processChange(int modelStart, int modelEnd,
String insertedText, String replacedText,
long beforeChangeModificationStamp,
long afterChangeModificationStamp) {
if (insertedText == null)
insertedText = ""; //$NON-NLS-1$
if (replacedText == null)
replacedText = ""; //$NON-NLS-1$
int length = insertedText.length();
if (fCurrent.fUndoModificationStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP)
fCurrent.fUndoModificationStamp = beforeChangeModificationStamp;
if (modelEnd < modelStart) {
int tmp = modelEnd;
modelEnd = modelStart;
modelStart = tmp;
}
if (modelStart == modelEnd) {
// text will be inserted
if ((length == 1) || isWhitespaceText(insertedText)) {
// by typing or whitespace
if (!fInserting
|| (modelStart != fCurrent.fStart
+ fTextBuffer.length())) {
fCurrent.fRedoModificationStamp = beforeChangeModificationStamp;
if (fCurrent.attemptCommit())
fCurrent.fUndoModificationStamp = beforeChangeModificationStamp;
fInserting = true;
}
if (fCurrent.fStart < 0)
fCurrent.fStart = fCurrent.fEnd = modelStart;
if (length > 0)
fTextBuffer.append(insertedText);
} else if (length > 0) {
// by pasting or model manipulation
fCurrent.fRedoModificationStamp = beforeChangeModificationStamp;
if (fCurrent.attemptCommit())
fCurrent.fUndoModificationStamp = beforeChangeModificationStamp;
fCurrent.fStart = fCurrent.fEnd = modelStart;
fTextBuffer.append(insertedText);
// fCurrent.fRedoModificationStamp = afterChangeModificationStamp;
// if ( fCurrent.attemptCommit() )
// fCurrent.fUndoModificationStamp = afterChangeModificationStamp;
}
} else {
if (length == 0) {
// text will be deleted by backspace or DEL key or empty
// clipboard
length = replacedText.length();
String[] delimiters = fDocument.getLegalLineDelimiters();
if ((length == 1)
|| TextUtilities.equals(delimiters, replacedText) > -1) {
// whereby selection is empty
if (fPreviousDelete.fStart == modelStart
&& fPreviousDelete.fEnd == modelEnd) {
// repeated DEL
// correct wrong settings of fCurrent
if (fCurrent.fStart == modelEnd
&& fCurrent.fEnd == modelStart) {
fCurrent.fStart = modelStart;
fCurrent.fEnd = modelEnd;
}
// append to buffer && extend edit range
fPreservedTextBuffer.append(replacedText);
++fCurrent.fEnd;
} else if (fPreviousDelete.fStart == modelEnd) {
// repeated backspace
// insert in buffer and extend edit range
fPreservedTextBuffer.insert(0, replacedText);
fCurrent.fStart = modelStart;
} else {
// either DEL or backspace for the first time
fCurrent.fRedoModificationStamp = beforeChangeModificationStamp;
if (fCurrent.attemptCommit())
fCurrent.fUndoModificationStamp = beforeChangeModificationStamp;
// as we can not decide whether it was DEL or backspace
// we initialize for backspace
fPreservedTextBuffer.append(replacedText);
fCurrent.fStart = modelStart;
fCurrent.fEnd = modelEnd;
}
fPreviousDelete.set(modelStart, modelEnd);
} else if (length > 0) {
// whereby selection is not empty
fCurrent.fRedoModificationStamp = beforeChangeModificationStamp;
if (fCurrent.attemptCommit())
fCurrent.fUndoModificationStamp = beforeChangeModificationStamp;
fCurrent.fStart = modelStart;
fCurrent.fEnd = modelEnd;
fPreservedTextBuffer.append(replacedText);
}
} else {
// text will be replaced
if (length == 1) {
length = replacedText.length();
String[] delimiters = fDocument.getLegalLineDelimiters();
if ((length == 1)
|| TextUtilities.equals(delimiters, replacedText) > -1) {
// because of overwrite mode or model manipulation
if (!fOverwriting
|| (modelStart != fCurrent.fStart
+ fTextBuffer.length())) {
fCurrent.fRedoModificationStamp = beforeChangeModificationStamp;
if (fCurrent.attemptCommit())
fCurrent.fUndoModificationStamp = beforeChangeModificationStamp;
fOverwriting = true;
}
if (fCurrent.fStart < 0)
fCurrent.fStart = modelStart;
fCurrent.fEnd = modelEnd;
fTextBuffer.append(insertedText);
fPreservedTextBuffer.append(replacedText);
fCurrent.fRedoModificationStamp = afterChangeModificationStamp;
return;
}
}
// because of typing or pasting whereby selection is not empty
fCurrent.fRedoModificationStamp = beforeChangeModificationStamp;
if (fCurrent.attemptCommit())
fCurrent.fUndoModificationStamp = beforeChangeModificationStamp;
fCurrent.fStart = modelStart;
fCurrent.fEnd = modelEnd;
fTextBuffer.append(insertedText);
fPreservedTextBuffer.append(replacedText);
}
}
// in all cases, the redo modification stamp is updated on the open
// text edit
fCurrent.fRedoModificationStamp = afterChangeModificationStamp;
}
/**
* Initialize the receiver.
*/
private void initialize() {
initializeUndoHistory();
// open up the current text edit
fCurrent = new UndoableRichTextChange(this);
fPreviousDelete = new UndoableRichTextChange(this);
fTextBuffer = new StringBuffer();
fPreservedTextBuffer = new StringBuffer();
addListeners();
}
/**
* Reset processChange state.
*
* @since 3.2
*/
private void resetProcessChangeState() {
fInserting = false;
fOverwriting = false;
fPreviousDelete.reinitialize();
}
/**
* Shutdown the receiver.
*/
private void shutdown() {
removeListeners();
fCurrent = null;
fPreviousDelete = null;
fTextBuffer = null;
fPreservedTextBuffer = null;
disposeUndoHistory();
}
/**
* Return whether or not any clients are connected to the receiver.
*
* @return <code>true</code> if the receiver is connected to clients,
* <code>false</code> if it is not
*/
boolean isConnected() {
if (fConnected == null)
return false;
return !fConnected.isEmpty();
}
/*
* @seeorg.eclipse.jface.text.IDocumentUndoManager#transferUndoHistory(
* IDocumentUndoManager)
*/
public void transferUndoHistory(IRichDocumentUndoManager manager) {
IUndoContext oldUndoContext = manager.getUndoContext();
// Get the history for the old undo context.
IUndoableOperation[] operations = OperationHistoryFactory
.getOperationHistory().getUndoHistory(oldUndoContext);
for (int i = 0; i < operations.length; i++) {
// First replace the undo context
IUndoableOperation op = operations[i];
if (op instanceof IContextReplacingOperation) {
((IContextReplacingOperation) op).replaceContext(
oldUndoContext, getUndoContext());
} else {
op.addContext(getUndoContext());
op.removeContext(oldUndoContext);
}
// Now update the manager that owns the text edit.
if (op instanceof UndoableRichTextChange) {
((UndoableRichTextChange) op).fDocumentUndoManager = this;
}
}
// Record the transfer itself as an undoable change.
// If the transfer results from some open operation, recording this change will
// cause our undo context to be added to the outer operation. If there is no
// outer operation, there will be a local change to signify the transfer.
// This also serves to synchronize the modification stamps with the documents.
IUndoableOperation op = OperationHistoryFactory.getOperationHistory()
.getUndoOperation(getUndoContext());
UndoableRichTextChange cmd = new UndoableRichTextChange(this);
cmd.fStart = cmd.fEnd = 0;
cmd.fText = cmd.fPreservedText = ""; //$NON-NLS-1$
if (fDocument instanceof IDocumentExtension4) {
cmd.fRedoModificationStamp = ((IDocumentExtension4) fDocument)
.getModificationStamp();
if (op instanceof UndoableRichTextChange) {
cmd.fUndoModificationStamp = ((UndoableRichTextChange) op).fRedoModificationStamp;
}
}
addToOperationHistory(cmd);
}
}