/** * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.client.editor; import org.waveprotocol.wave.client.debug.logger.DomLogger; import org.waveprotocol.wave.client.editor.content.RangeHelper; import org.waveprotocol.wave.client.editor.selection.content.SelectionHelper; import org.waveprotocol.wave.common.logging.LoggerBundle; import org.waveprotocol.wave.common.logging.AbstractLogger.Level; import org.waveprotocol.wave.model.undo.UndoManagerPlus; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.util.FocusedRange; import org.waveprotocol.wave.model.operation.SilentOperationSink; import org.waveprotocol.wave.model.util.Pair; import org.waveprotocol.wave.model.util.Preconditions; import java.util.Stack; /** * Implementation of an EditorUndoManager. * * Keeps track of operations applied to a document and the selection at each * checkpoint, so that old states can be restored with undo/redo. * * */ public class EditorUndoManagerImpl implements EditorUndoManager { private final SilentOperationSink<DocOp> sink; private final SelectionHelper selectionHelper; private final Stack<FocusedRange> undoSelectionStack = new Stack<FocusedRange>(); private final Stack<FocusedRange> redoSelectionStack = new Stack<FocusedRange>(); private final UndoManagerPlus<DocOp> undoManager; private FocusedRange pendingCheckpoint; private static final LoggerBundle logger = new DomLogger("undo"); private static final FocusedRange UNKNOWN_SELECTION = new FocusedRange(0, 0); private boolean bypass = false; public EditorUndoManagerImpl(UndoManagerPlus<DocOp> undoManager, SilentOperationSink<DocOp> sink, SelectionHelper selectionHelper) { Preconditions.checkNotNull(undoManager, "UndoManager must not be null"); Preconditions.checkNotNull(sink, "Op sink must not be null"); Preconditions.checkNotNull(selectionHelper, "Selection helper must not be null"); this.sink = sink; this.undoManager = undoManager; this.selectionHelper = selectionHelper; } @Override public void undoableOp(DocOp op) { if (bypass) { return; } if (pendingCheckpoint != null) { if (logger.trace().shouldLog()) { logger.log(Level.TRACE, "checkpointing, selection known?" + (pendingCheckpoint != UNKNOWN_SELECTION) + " undo selection stack size: " + undoSelectionStack.size()); } if (pendingCheckpoint == UNKNOWN_SELECTION) { pendingCheckpoint = selectionHelper.getSelectionRange(); if (pendingCheckpoint == null) { pendingCheckpoint = UNKNOWN_SELECTION; } } undoManager.checkpoint(); undoSelectionStack.push(pendingCheckpoint); pendingCheckpoint = null; } undoManager.undoableOp(op); if (redoSelectionStack.size() != 0) { if (logger.trace().shouldLog()) { logger.log(Level.TRACE, "redoStack cleared " + redoSelectionStack.size()); } redoSelectionStack.clear(); } } @Override public void nonUndoableOp(DocOp op) { if (bypass) { return; } undoManager.nonUndoableOp(op); } @Override public void maybeCheckpoint() { if (pendingCheckpoint == null) { pendingCheckpoint = UNKNOWN_SELECTION; } } @Override public void maybeCheckpoint(int startLocation, int endLocation) { pendingCheckpoint = new FocusedRange(startLocation, endLocation); } @Override public void undo() { Pair<DocOp, DocOp> pair = undoManager.undoPlus(); if (pair == null || pair.first == null) { if (logger.trace().shouldLog()) { logger.log(Level.TRACE, "cannot undo " + undoSelectionStack.size()); } return; } DocOp undo = pair.first; DocOp transformedNonUndoable = pair.second; { FocusedRange selection = selectionHelper.getSelectionRange(); if (selection != null) { redoSelectionStack.push(selection); } else { redoSelectionStack.push(UNKNOWN_SELECTION); } } bypassUndoStack(undo); restoreSelectionAfterUndoRedo(transformedNonUndoable, undoSelectionStack); logger.trace().log("Undoing!"); } @Override public void redo() { Pair<DocOp, DocOp> pair = undoManager.redoPlus(); if (pair == null || pair.first == null) { if (logger.trace().shouldLog()) { logger.log(Level.TRACE, "cannot redo " + redoSelectionStack.size()); } return; } DocOp redo = pair.first; DocOp transformedNonUndoable = pair.second; { FocusedRange selection = selectionHelper.getSelectionRange(); if (selection != null) { undoSelectionStack.push(selection); } else { undoSelectionStack.push(UNKNOWN_SELECTION); } } bypassUndoStack(redo); restoreSelectionAfterUndoRedo(transformedNonUndoable, redoSelectionStack); logger.trace().log("Redoing!"); } /** * Applies an op locally and send it bypassing the undo stack. This is * neccessary with operations popped from the undoManager as they are * automatically applied. * * @param op */ private void bypassUndoStack(DocOp op) { bypass = true; try { sink.consume(op); } finally { bypass = false; } } private FocusedRange restoreSelectionAfterUndoRedo(DocOp transformedNonUndoable, Stack<FocusedRange> selectionStack) { if (selectionStack.isEmpty()) { logger.log(Level.ERROR, "SelectionStack empty! This probably shouldn't be reached, but we can live with it."); return null; } FocusedRange selection = selectionStack.pop(); if (selection == UNKNOWN_SELECTION) { logger.log(Level.TRACE, "unknown selection"); return null; } else { if (transformedNonUndoable != null) { selection = RangeHelper.applyModifier(transformedNonUndoable, selection); } selectionHelper.setSelectionRange(selection); return selection; } } }