// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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 com.google.collide.client.editor;
import com.google.collide.client.editor.selection.SelectionModel;
import com.google.collide.dto.DocOp;
import com.google.collide.dto.client.ClientDocOpFactory;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.document.TextChange;
import com.google.collide.shared.document.util.PositionUtils;
import com.google.collide.shared.ot.Composer;
import com.google.collide.shared.ot.DocOpApplier;
import com.google.collide.shared.ot.DocOpUtils;
import com.google.collide.shared.ot.Inverter;
import com.google.collide.shared.ot.Transformer;
import com.google.collide.shared.ot.Composer.ComposeException;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerRegistrar;
import org.waveprotocol.wave.model.operation.OperationPair;
import org.waveprotocol.wave.model.operation.TransformException;
import org.waveprotocol.wave.model.undo.UndoManagerImpl;
import org.waveprotocol.wave.model.undo.UndoManagerImpl.Algorithms;
import org.waveprotocol.wave.model.undo.UndoManagerPlus;
import java.util.List;
// TODO: restore selection/cursor
/**
* A class to manage the editor's undo/redo functionality.
*
*/
public class EditorUndoManager {
/**
* Delegates to the document operation algorithms when requested by the Wave
* undo library.
*/
private static final UndoManagerImpl.Algorithms<DocOp> ALGORITHMS = new Algorithms<DocOp>() {
@Override
public DocOp invert(DocOp operation) {
return Inverter.invert(ClientDocOpFactory.INSTANCE, operation);
}
@Override
public DocOp compose(List<DocOp> operationsReverse) {
try {
return Composer.compose(ClientDocOpFactory.INSTANCE, operationsReverse);
} catch (ComposeException e) {
throw new RuntimeException(e);
}
}
@Override
public OperationPair<DocOp> transform(DocOp op1, DocOp op2) throws TransformException {
try {
com.google.collide.shared.ot.OperationPair ourPair =
Transformer.transform(ClientDocOpFactory.INSTANCE, op1, op2);
return new OperationPair<DocOp>(ourPair.clientOp(), ourPair.serverOp());
} catch (com.google.collide.shared.ot.Transformer.TransformException e) {
throw new TransformException(e);
}
}
};
public static EditorUndoManager create(Editor editor, Document document,
SelectionModel selection) {
return new EditorUndoManager(editor, document, selection,
new UndoManagerImpl<DocOp>(ALGORITHMS));
}
/*
* TODO: think about which other events should cause a
* checkpoint. Push out to a toplevel class if it gets complicated.
*/
/**
* Produces undo checkpoints at opportune times, such as when the user
* explicitly moves the cursor (arrow keys or mouse click) or when the user's
* text mutations change from delete to insert.
*/
private class CheckpointProducer
implements
SelectionModel.CursorListener,
Editor.BeforeTextListener,
Editor.TextListener {
private TextChange.Type previousTextChangeType;
@Override
public void onCursorChange(LineInfo lineInfo, int column, boolean isExplicitChange) {
if (isExplicitChange) {
undoManager.checkpoint();
}
}
@Override
public void onBeforeTextChange(TextChange textChange) {
if (previousTextChangeType != textChange.getType()) {
undoManager.checkpoint();
}
previousTextChangeType = textChange.getType();
}
@Override
public void onTextChange(TextChange textChange) {
if (textChange.getText().contains("\n")) {
/*
* Checkpoint after newlines which tends to be a good granularity for
* typical editor use
*/
undoManager.checkpoint();
}
}
}
private final CheckpointProducer checkpointProducer = new CheckpointProducer();
private final Document document;
private final Document.TextListener documentTextListener = new Document.TextListener() {
@Override
public void onTextChange(Document document, JsonArray<TextChange> textChanges) {
if (isMutatingDocument || editor.getEditorDocumentMutator().isMutatingDocument()) {
// We will handle this text change in the editor text change callback
return;
}
for (int i = 0, n = textChanges.size(); i < n; i++) {
TextChange textChange = textChanges.get(i);
// This is a collaborator doc op, which is not undoable by us
undoManager.nonUndoableOp(DocOpUtils.createFromTextChange(ClientDocOpFactory.INSTANCE,
textChange));
}
}
};
private final Editor editor;
private final Editor.TextListener editorTextListener = new Editor.TextListener() {
@Override
public void onTextChange(TextChange textChange) {
if (isMutatingDocument) {
// We caused this text change, so don't handle it
return;
}
// This is a user document mutation that is undoable
undoManager.undoableOp(DocOpUtils.createFromTextChange(ClientDocOpFactory.INSTANCE,
textChange));
}
};
private boolean isMutatingDocument;
private final JsonArray<ListenerRegistrar.Remover> listenerRemovers =
JsonCollections.createArray();
private final SelectionModel selection;
private final UndoManagerPlus<DocOp> undoManager;
private EditorUndoManager(Editor editor, Document document, SelectionModel selection,
UndoManagerPlus<DocOp> undoManager) {
this.document = document;
this.editor = editor;
this.selection = selection;
this.undoManager = undoManager;
listenerRemovers.add(document.getTextListenerRegistrar().add(documentTextListener));
listenerRemovers.add(editor.getTextListenerRegistrar().add(editorTextListener));
listenerRemovers.add(selection.getCursorListenerRegistrar().add(checkpointProducer));
listenerRemovers.add(editor.getBeforeTextListenerRegistrar().add(checkpointProducer));
listenerRemovers.add(editor.getTextListenerRegistrar().add(checkpointProducer));
}
boolean isMutatingDocument() {
return isMutatingDocument;
}
void undo() {
DocOp undoDocOp = undoManager.undo();
if (undoDocOp == null) {
return;
}
applyToDocument(undoDocOp);
}
void redo() {
DocOp redoDocOp = undoManager.redo();
if (redoDocOp == null) {
return;
}
applyToDocument(redoDocOp);
}
void teardown() {
for (int i = 0, n = listenerRemovers.size(); i < n; i++) {
listenerRemovers.get(i).remove();
}
}
private void applyToDocument(DocOp docOp) {
JsonArray<TextChange> textChanges;
isMutatingDocument = true;
try {
/*
* Mutate via the document (instead of editor) since we don't want this to
* be affected by autoindentation, etc. We also don't want to replace the
* text in the selection, and the document's mutator will never do this.
*/
textChanges = DocOpApplier.apply(docOp, document, document);
} finally {
isMutatingDocument = false;
}
// a retain only doc-op will not produce a text-change
if (textChanges.size() == 0) {
return;
}
/*
* There can theoretically be multiple text changes, but we just set
* selection to the first
*/
TextChange textChange = textChanges.get(0);
Position endPosition =
new Position(new LineInfo(textChange.getEndLine(), textChange.getEndLineNumber()),
textChange.getEndColumn());
if (textChange.getType() == TextChange.Type.INSERT) {
endPosition = PositionUtils.getPosition(endPosition, 1);
}
selection.setSelection(new LineInfo(textChange.getLine(), textChange.getLineNumber()),
textChange.getColumn(), endPosition.getLineInfo(),
endPosition.getColumn());
}
}