org.fxmisc.undo.impl.UndoManagerImpl Maven / Gradle / Ivy
package org.fxmisc.undo.impl;
import java.time.Duration;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ObservableBooleanValue;
import org.fxmisc.undo.UndoManager;
import org.fxmisc.undo.impl.ChangeQueue.QueuePosition;
import org.reactfx.EventSource;
import org.reactfx.EventStream;
import org.reactfx.Subscription;
import org.reactfx.SuspendableNo;
import org.reactfx.value.Val;
import org.reactfx.value.ValBase;
/**
* Implementation for {@link UndoManager} for single changes. For multiple changes, see
* {@link MultiChangeUndoManagerImpl}.
*
* @param the type of change to undo/redo
*/
public class UndoManagerImpl implements UndoManager {
private class UndoPositionImpl implements UndoPosition {
private final QueuePosition queuePos;
UndoPositionImpl(QueuePosition queuePos) {
this.queuePos = queuePos;
}
@Override
public void mark() {
mark = queuePos;
canMerge = false;
atMarkedPosition.invalidate();
}
@Override
public boolean isValid() {
return queuePos.isValid();
}
}
private final ChangeQueue queue;
private final Function super C, ? extends C> invert;
private final Consumer apply;
private final BiFunction> merge;
private final Predicate isIdentity;
private final Subscription subscription;
private final SuspendableNo performingAction = new SuspendableNo();
private final EventSource invalidationRequests = new EventSource();
private final Val nextUndo = new ValBase() {
@Override protected Subscription connect() { return invalidationRequests.subscribe(x -> invalidate()); }
@Override protected C computeValue() { return queue.hasPrev() ? queue.peekPrev() : null; }
};
private final Val nextRedo = new ValBase() {
@Override protected Subscription connect() { return invalidationRequests.subscribe(x -> invalidate()); }
@Override protected C computeValue() { return queue.hasNext() ? queue.peekNext() : null; }
};
private final BooleanBinding atMarkedPosition = new BooleanBinding() {
{ invalidationRequests.addObserver(x -> this.invalidate()); }
@Override
protected boolean computeValue() {
return mark.equals(queue.getCurrentPosition());
}
};
private boolean canMerge;
private QueuePosition mark;
private C expectedChange = null;
public UndoManagerImpl(
ChangeQueue queue,
Function super C, ? extends C> invert,
Consumer apply,
BiFunction> merge,
Predicate isIdentity,
EventStream changeSource) {
this(queue, invert, apply, merge, isIdentity, changeSource, Duration.ZERO);
}
public UndoManagerImpl(
ChangeQueue queue,
Function super C, ? extends C> invert,
Consumer apply,
BiFunction> merge,
Predicate isIdentity,
EventStream changeSource,
Duration preventMergeDelay) {
this.queue = queue;
this.invert = invert;
this.apply = apply;
this.merge = merge;
this.isIdentity = isIdentity;
this.mark = queue.getCurrentPosition();
Subscription mainSub = changeSource.subscribe(this::changeObserved);
if (preventMergeDelay.isZero() || preventMergeDelay.isNegative()) {
subscription = mainSub;
} else {
Subscription sub2 = changeSource.successionEnds(preventMergeDelay).subscribe(ignore -> preventMerge());
subscription = mainSub.and(sub2);
}
}
@Override
public void close() {
subscription.unsubscribe();
}
@Override
public boolean undo() {
return applyChange(isUndoAvailable(), () -> invert.apply(queue.prev()));
}
@Override
public boolean redo() {
return applyChange(isRedoAvailable(), queue::next);
}
@Override
public Val nextUndoProperty() {
return nextUndo;
}
@Override
public Val nextRedoProperty() {
return nextRedo;
}
@Override
public boolean isUndoAvailable() {
return nextUndo.isPresent();
}
@Override
public Val undoAvailableProperty() {
return nextUndo.map(c -> true).orElseConst(false);
}
@Override
public boolean isRedoAvailable() {
return nextRedo.isPresent();
}
@Override
public Val redoAvailableProperty() {
return nextRedo.map(c -> true).orElseConst(false);
}
@Override
public boolean isPerformingAction() {
return performingAction.get();
}
@Override
public ObservableBooleanValue performingActionProperty() {
return performingAction;
}
@Override
public boolean isAtMarkedPosition() {
return atMarkedPosition.get();
}
@Override
public ObservableBooleanValue atMarkedPositionProperty() {
return atMarkedPosition;
}
@Override
public UndoPosition getCurrentPosition() {
return new UndoPositionImpl(queue.getCurrentPosition());
}
@Override
public void preventMerge() {
canMerge = false;
}
@Override
public void forgetHistory() {
queue.forgetHistory();
invalidateProperties();
}
/**
* Helper method for reducing code duplication
*
* @param isChangeAvailable same as `isUndoAvailable()` [Undo] or `isRedoAvailable()` [Redo]
* @param changeToApply same as `invert.apply(queue.prev())` [Undo] or `queue.next()` [Redo]
*/
private boolean applyChange(boolean isChangeAvailable, Supplier changeToApply) {
if (isChangeAvailable) {
canMerge = false;
// perform change
C change = changeToApply.get();
this.expectedChange = change;
performingAction.suspendWhile(() -> apply.accept(change));
if(this.expectedChange != null) {
throw new IllegalStateException("Expected change not received:\n"
+ this.expectedChange);
}
invalidateProperties();
return true;
} else {
return false;
}
}
private void changeObserved(C change) {
if(expectedChange == null) {
if (!isIdentity.test(change)) {
addChange(change);
}
} else if(expectedChange.equals(change)) {
expectedChange = null;
} else {
throw new IllegalArgumentException("Unexpected change received."
+ "\nExpected:\n" + expectedChange
+ "\nReceived:\n" + change);
}
}
@SuppressWarnings("unchecked")
private void addChange(C change) {
if(canMerge && queue.hasPrev()) {
C prev = queue.prev();
// attempt to merge the changes
Optional merged = merge.apply(prev, change);
if(merged.isPresent()) {
if (isIdentity.test(merged.get())) {
canMerge = false;
queue.push(); // clears the future
} else {
canMerge = true;
queue.push(merged.get());
}
} else {
canMerge = true;
queue.next();
queue.push(change);
}
} else {
queue.push(change);
canMerge = true;
}
invalidateProperties();
}
private void invalidateProperties() {
invalidationRequests.push(null);
}
}