# HG changeset patch # Parent 0be35ff56c6af38a25cc99546ea88d07acd21cf2 diff --git a/openide.text/src/org/openide/text/CloneableEditorSupport.java b/openide.text/src/org/openide/text/CloneableEditorSupport.java --- a/openide.text/src/org/openide/text/CloneableEditorSupport.java +++ b/openide.text/src/org/openide/text/CloneableEditorSupport.java @@ -93,8 +93,11 @@ import javax.swing.event.DocumentListener; import javax.swing.event.UndoableEditEvent; import javax.swing.text.*; +import javax.swing.undo.AbstractUndoableEdit; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; +import javax.swing.undo.CompoundEdit; +import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEdit; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; @@ -120,6 +123,10 @@ * but does not implement * those interfaces. It is up to the subclass to decide which interfaces * really implement and which not. +*

+* This class supports collecting multiple edits into a group which is treated +* as a single edit by undo/redo. Send {@BEGIN_COMIT_GROUP} and +* {@END_COMIT_GROUP} to UndoableEditListener. These must always be paired. * * @author Jaroslav Tulach */ @@ -128,6 +135,17 @@ /** Common name for editor mode. */ public static final String EDITOR_MODE = "editor"; // NOI18N + /** + * Start a group of edits which will be committed as a single edit + * for purpose of undo/redo. + * Nesting semantics are that any BEGIN_COMIT_GROUP and + * END_COMIT_GROUP delimits a comit-group. + * While coalescing edits, any undo/redo/save implicitly delimits + * a comit-group. + */ + public static final UndoableEdit BEGIN_COMIT_GROUP = UndoGroupManager.BEGIN_COMIT_GROUP; + /** End a group of edits. */ + public static final UndoableEdit END_COMIT_GROUP = UndoGroupManager.END_COMIT_GROUP; private static final String PROP_PANE = "CloneableEditorSupport.Pane"; //NOI18N private static final int DOCUMENT_NO = 0; private static final int DOCUMENT_LOADING = 1; @@ -2986,7 +3004,9 @@ } /** Generic undoable edit that delegates to the given undoable edit. */ - private class FilterUndoableEdit implements UndoableEdit { + private class FilterUndoableEdit + implements UndoableEdit, UndoGroupManager.SeparateEdit + { protected UndoableEdit delegate; FilterUndoableEdit() { @@ -3187,7 +3207,7 @@ /** An improved version of UndoRedo manager that locks document before * doing any other operations. */ - private final static class CESUndoRedoManager extends UndoRedo.Manager { + private final static class CESUndoRedoManager extends UndoGroupManager { private CloneableEditorSupport support; public CESUndoRedoManager(CloneableEditorSupport c) { @@ -3421,6 +3441,292 @@ } } + /** + * UndoGroupManager extends {@link UndoManager} + * and allows explicit control of what + * UndoableEdits are coalesced into compound edits, + * rather than using the rules defined by the edits themselves. + * Groups are defined using BEGIN_COMIT_GROUP and END_COMIT_GROUP. + * Send these to UndoableEditListener. These must always be paired. + *

+ * These use cases are supported. + *

+ *
    + *
  1. Default behavior is defined by {@link UndoManager}.
  2. + *
  3. UnddoableEdits issued between {@link #BEGIN_COMIT_GROUP} + * and {@link END_COMIT_GROUP} are placed into a single + * {@link CompoundEdit}. + * Thus undo() and redo() treat them + * as a single undo/redo.
  4. + *
  5. Use {@link comitUndoGroup} to commit accumulated + * UndoableEdits into a single CompoundEdit + * (and to continue accumulating); + * an application could do this at strategic points, such as EndOfLine + * input or cursor movement. In this way, the application can accumulate + * large chunks.
  6. + *
  7. BEGIN/END nest.
  8. + *
+ * @see UndoManager + */ + private static class UndoGroupManager extends UndoRedo.Manager { + /** signals that edits are being accumulated */ + private int buildUndoGroup; + /** accumulate edits here in undoGroup */ + private CompoundEdit undoGroup; + + /** + * Start a group of edits which will be committed as a single edit + * for purpose of undo/redo. + * Nesting semantics are that any BEGIN_COMIT_GROUP and + * END_COMIT_GROUP delimits a comit-group. + * While coalescing edits, any undo/redo/save implicitly delimits + * a comit-group. + */ + public static final UndoableEdit BEGIN_COMIT_GROUP = new ComitGroupEdit(); + /** End a group of edits. */ + public static final UndoableEdit END_COMIT_GROUP = new ComitGroupEdit(); + + /** SeparateEdit tags an UndoableEdit so the + * UndoGroupManager does not coalesce it. + */ + public interface SeparateEdit { + } + + private static class ComitGroupEdit extends AbstractUndoableEdit { + @Override + public boolean isSignificant() { + return false; + } + } + + @Override + public void undoableEditHappened(UndoableEditEvent ue) + { + if(ue.getEdit() == BEGIN_COMIT_GROUP) { + beginUndoGroup(); + } else if(ue.getEdit() == END_COMIT_GROUP) { + endUndoGroup(); + } else { + super.undoableEditHappened(ue); + } + } + + /** + * Direct this UndoGroupManager to begin coalescing any + * UndoableEdits that are added into a CompoundEdit. + *

If edits are already being coalesced and some have been + * accumulated, they are commited as an atomic group and a new + * group is started. + * @see #addEdit + * @see #endUndoGroup + */ + private synchronized void beginUndoGroup() { + commitUndoGroup(); + ERR.log(Level.FINE, "beginUndoGroup: nesting {0}", buildUndoGroup); + buildUndoGroup++; + } + + /** + * Direct this UndoGroupManager to stop coalescing edits. + * Until beginUndoGroupManager is invoked, + * any received UndoableEdits are added singly. + *

+ * This has no effect if edits are not being coalesced, for example + * if beginUndoGroup has not been called. + */ + private synchronized void endUndoGroup() { + buildUndoGroup--; + ERR.log(Level.FINE, "endUndoGroup: nesting {0}", buildUndoGroup); + if(buildUndoGroup < 0) { + ERR.log(Level.INFO, null, new Exception("endUndoGroup without beginUndoGroup")); + buildUndoGroup = 0; + } + // slam buildUndoGroup to 0 to disable nesting + commitUndoGroup(); + } + + /** + * Commit any accumulated UndoableEdits as an atomic + * undo/redo group. {@link CompoundEdit#end} + * is invoked on the CompoundEdit and it is added as a single + * UndoableEdit to this UndoManager. + *

+ * If edits are currently being coalesced, a new undo group is started. + * This has no effect if edits are not being coalesced, for example + * beginUndoGroup has not been called. + */ + private synchronized void commitUndoGroup() { + if(undoGroup == null) { + return; + } + // super.addEdit may end up in this.addEdit, + // so buildUndoGroup must be false + int saveBuildUndoGroup = buildUndoGroup; + buildUndoGroup = 0; + + undoGroup.end(); + super.addEdit(undoGroup); + undoGroup = null; + + buildUndoGroup = saveBuildUndoGroup; + } + + /** Add this edit separately, not part of a group. + * @return super.addEdit + */ + private boolean commitAddEdit(UndoableEdit anEdit) { + commitUndoGroup(); + + int saveBuildUndoGroup = buildUndoGroup; + buildUndoGroup = 0; + boolean f = super.addEdit(anEdit); + //boolean f = addEdit(undoGroup); + buildUndoGroup = saveBuildUndoGroup; + return f; + } + + /** + * If this UndoManager is coalescing edits then add + * anEdit to the accumulating CompoundEdit. + * Otherwise, add it to this UndoManager. In either case the + * edit is saved for later undo or redo. + * @return {@inheritDoc} + * @see #beginUndoGroup + * @see #endUndoGroup + */ + @Override + public synchronized boolean addEdit(UndoableEdit anEdit) { + if(!isInProgress()) + return false; + + if(buildUndoGroup > 0) { + if(anEdit instanceof SeparateEdit) + return commitAddEdit(anEdit); + if(undoGroup == null) + undoGroup = new CompoundEdit(); + return undoGroup.addEdit(anEdit); + } else { + return super.addEdit(anEdit); + } + } + + /** {@inheritDoc} */ + @Override + public synchronized void discardAllEdits() { + commitUndoGroup(); + super.discardAllEdits(); + } + + // + // TODO: limits + // + + /** {@inheritDoc} */ + @Override + public synchronized void undoOrRedo() { + commitUndoGroup(); + super.undoOrRedo(); + } + + /** {@inheritDoc} */ + @Override + public synchronized boolean canUndoOrRedo() { + if(undoGroup != null) + return true; + return super.canUndoOrRedo(); + } + + /** {@inheritDoc} */ + @Override + public synchronized void undo() { + commitUndoGroup(); + super.undo(); + } + + /** {@inheritDoc} */ + @Override + public synchronized boolean canUndo() { + if(undoGroup != null) + return true; + return super.canUndo(); + } + + /** {@inheritDoc} */ + @Override + public synchronized void redo() { + if(undoGroup != null) + throw new CannotRedoException(); + super.redo(); + } + + /** {@inheritDoc} */ + @Override + public synchronized boolean canRedo() { + if(undoGroup != null) + return false; + return super.canRedo(); + } + + /** {@inheritDoc} */ + @Override + public synchronized void end() { + commitUndoGroup(); + super.end(); + } + + /** {@inheritDoc} */ + @Override + public synchronized String getUndoOrRedoPresentationName() { + if(undoGroup != null) + return undoGroup.getUndoPresentationName(); + return super.getUndoOrRedoPresentationName(); + } + + /** {@inheritDoc} */ + @Override + public synchronized String getUndoPresentationName() { + if(undoGroup != null) + return undoGroup.getUndoPresentationName(); + return super.getUndoPresentationName(); + } + + /** {@inheritDoc} */ + @Override + public synchronized String getRedoPresentationName() { + if(undoGroup != null) + return undoGroup.getRedoPresentationName(); + return super.getRedoPresentationName(); + } + + /** {@inheritDoc} */ + @Override + public boolean isSignificant() { + if(undoGroup != null && undoGroup.isSignificant()) { + return true; + } + return super.isSignificant(); + } + + /** {@inheritDoc} */ + @Override + public synchronized void die() { + commitUndoGroup(); + super.die(); + } + + /** {@inheritDoc} */ + @Override + public String getPresentationName() { + if(undoGroup != null) + return undoGroup.getPresentationName(); + return super.getPresentationName(); + } + + // The protected methods are only accessed from + // synchronized methods that do commitUndoGroup + // so they do not need to be overridden in this class + } + /** Special runtime exception that holds the original I/O failure. */ static final class DelegateIOExc extends IllegalStateException {