/*
* Copyright (C) 2015-2017 Emanuel Moecklin
*
* 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.onegravity.rteditor;
import android.annotation.SuppressLint;
import android.text.Spannable;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
/**
* This class manages Operations for multiple rich text editors.
* It's used by the RTManager to undo/redo operations.
*/
@SuppressLint("UseSparseArrays")
class RTOperationManager {
/*
* Maximum number of operations to put in the undo/redo stack
*/
private static final int MAX_NR_OF_OPERATIONS = 50;
/*
* two operations performed in this time frame (in ms) are considered one
* operation
*/
private static final int TIME_BETWEEN_OPERATIONS = 300;
/*
* The undo/redo stacks by editor Id
*/
private Map<Integer, Stack<Operation>> mUndoStacks = new HashMap<Integer, Stack<Operation>>();
private Map<Integer, Stack<Operation>> mRedoStacks = new HashMap<Integer, Stack<Operation>>();
;
// ****************************************** Operation Classes *******************************************
/**
* An atomic operation in the rich text editor.
* If two operations are performed within a certain time frame
* they will be considered as one operation and un-done/re-done together.
*/
private abstract static class Operation {
protected long mTimestamp;
private int mSelStartBefore;
private int mSelEndBefore;
private Spannable mBefore;
private int mSelStartAfter;
private int mSelEndAfter;
private Spannable mAfter;
Operation(Spannable before, Spannable after, int selStartBefore, int selEndBefore, int selStartAfter, int selEndAfter) {
mSelStartBefore = selStartBefore;
mSelEndBefore = selEndBefore;
mSelStartAfter = selStartAfter;
mSelEndAfter = selEndAfter;
mBefore = before;
mAfter = after;
mTimestamp = System.currentTimeMillis();
}
boolean canMerge(Operation other) {
return Math.abs(mTimestamp - other.mTimestamp) < TIME_BETWEEN_OPERATIONS;
}
Operation merge(Operation previousOp) {
mBefore = previousOp.mBefore;
mSelStartBefore = previousOp.mSelStartBefore;
mSelEndBefore = previousOp.mSelEndBefore;
return this;
}
void undo(RTEditText editor) {
editor.ignoreTextChanges();
editor.setText(mBefore);
editor.setSelection(mSelStartBefore, mSelEndBefore);
editor.registerTextChanges();
}
void redo(RTEditText editor) {
editor.ignoreTextChanges();
editor.setText(mAfter);
editor.setSelection(mSelStartAfter, mSelEndAfter);
editor.registerTextChanges();
}
}
static class TextChangeOperation extends Operation {
TextChangeOperation(Spannable before, Spannable after, int selStartBefore, int selEndBefore, int selStartAfter, int selEndAfter) {
super(before, after, selStartBefore, selEndBefore, selStartAfter, selEndAfter);
}
}
// ****************************************** execute/undo/redo/flush *******************************************
/**
* Call this when an operation is performed to add it to the undo stack.
*
* @param editor The rich text editor the operation was performed on
* @param op The Operation that was performed
*/
synchronized void executed(RTEditText editor, Operation op) {
Stack<Operation> undoStack = getUndoStack(editor);
Stack<Operation> redoStack = getRedoStack(editor);
// if operations are executed in a quick succession we "merge" them to have but one
// -> saves memory and makes more sense from a user perspective (each key stroke an undo? -> no way)
while (!undoStack.empty() && op.canMerge(undoStack.peek())) {
Operation previousOp = undoStack.pop();
op.merge(previousOp);
}
push(op, undoStack);
redoStack.clear();
}
/**
* Undo the last operation for a specific rich text editor
*
* @param editor Undo the last operation for this rich text editor
*/
synchronized void undo(RTEditText editor) {
Stack<Operation> undoStack = getUndoStack(editor);
if (!undoStack.empty()) {
Stack<Operation> redoStack = getRedoStack(editor);
Operation op = undoStack.pop();
push(op, redoStack);
op.undo(editor);
while (!undoStack.empty() && op.canMerge(undoStack.peek())) {
op = undoStack.pop();
push(op, redoStack);
op.undo(editor);
}
}
}
/**
* Re-do the last undone operation for a specific rich text editor
*
* @param editor Re-do an operation for this rich text editor
*/
synchronized void redo(RTEditText editor) {
Stack<Operation> redoStack = getRedoStack(editor);
if (!redoStack.empty()) {
Stack<Operation> undoStack = getUndoStack(editor);
Operation op = redoStack.pop();
push(op, undoStack);
op.redo(editor);
while (!redoStack.empty() && op.canMerge(redoStack.peek())) {
op = redoStack.pop();
push(op, undoStack);
op.redo(editor);
}
}
}
/**
* Flush all operations for a specific rich text editor (method unused at the moment)
*
* @param editor This rich text editor's operations will be flushed
*/
synchronized void flushOperations(RTEditText editor) {
Stack<Operation> undoStack = getUndoStack(editor);
Stack<Operation> redoStack = getRedoStack(editor);
undoStack.clear();
redoStack.clear();
}
// ****************************************** Private Methods *******************************************
private void push(Operation op, Stack<Operation> stack) {
if (stack.size() >= MAX_NR_OF_OPERATIONS) {
stack.remove(0);
}
stack.push(op);
}
private Stack<Operation> getUndoStack(RTEditText editor) {
return getStack(mUndoStacks, editor);
}
private Stack<Operation> getRedoStack(RTEditText editor) {
return getStack(mRedoStacks, editor);
}
private Stack<Operation> getStack(Map<Integer, Stack<Operation>> stacks, RTEditText editor) {
Stack<Operation> stack = stacks.get(editor.getId());
if (stack == null) {
stack = new Stack<Operation>();
stacks.put(editor.getId(), stack);
}
return stack;
}
}