Skip to content

Commit

Permalink
Merge pull request #530 from JordanMartinez/preventUndoMergeOnInactivity
Browse files Browse the repository at this point in the history
Stop next change from merging with previous one after inactive period.
  • Loading branch information
JordanMartinez committed Jun 22, 2017
2 parents c73aacb + 1b90a9a commit 51b2306
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.fxmisc.richtext.Caret;
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
import org.fxmisc.richtext.model.NavigationActions;
import org.fxmisc.richtext.util.UndoUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.fxmisc.richtext.view;

import org.fxmisc.richtext.InlineCssTextAreaAppTest;
import org.fxmisc.richtext.util.UndoUtils;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class UndoManagerTests extends InlineCssTextAreaAppTest {

@Test
public void preventMergeOfIncomingChangeAfterPeriodOfUserInactivity() {
String text1 = "text1";
String text2 = "text2";

long periodOfUserInactivity = UndoUtils.DEFAULT_PREVENT_MERGE_DELAY.toMillis() + 300L;

write(text1);
sleep(periodOfUserInactivity);
write(text2);

interact(area::undo);
assertEquals(text1, area.getText());

interact(area::undo);
assertEquals("", area.getText());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package org.fxmisc.richtext.util;

import javafx.beans.value.ObservableBooleanValue;
import org.fxmisc.undo.UndoManager;
import org.reactfx.EventStream;
import org.reactfx.Subscription;
import org.reactfx.value.Val;

import java.time.Duration;

/**
* A wrapper around an {@link UndoManager} that prevents the next emitted change from merging with the previous
* one after a period of inactivity (i.e., the UndoManager's {@code changeSource} has not emitted an event
* after a specified period of time.
*
* @param <C> the type of change the UndoManager can undo/redo
*/
final class UndoManagerInactivityWrapper<C> implements UndoManager<C> {

private final UndoManager<C> delegate;
private final Subscription subscription;

/**
* Wraps an {@link UndoManager} and prevents the next emitted change from merging with the previous one
* after a period of inactivity (i.e., the {@code changeSource} has not emitted an event for
* {@code preventMergeDelay}). <b>Note:</b> there is no check that insures that the {@code changeSource}
* parameter is the same one used by the {@code undoManager} parameter
*/
public UndoManagerInactivityWrapper(UndoManager<C> undoManager, EventStream<C> changeSource, Duration preventMergeDelay) {
this.delegate = undoManager;
subscription = changeSource.successionEnds(preventMergeDelay).subscribe(ignore -> preventMerge());
}

@Override
public boolean undo() {
return delegate.undo();
}

@Override
public boolean redo() {
return delegate.redo();
}

@Override
public Val<Boolean> undoAvailableProperty() {
return delegate.undoAvailableProperty();
}

@Override
public boolean isUndoAvailable() {
return delegate.isUndoAvailable();
}

@Override
public Val<C> nextToUndoProperty() {
return delegate.nextToUndoProperty();
}

@Override
public C getNextToUndo() {
return delegate.getNextToUndo();
}

@Override
public Val<C> nextToRedoProperty() {
return delegate.nextToRedoProperty();
}

@Override
public C getNextToRedo() {
return delegate.getNextToRedo();
}

@Override
public Val<Boolean> redoAvailableProperty() {
return delegate.redoAvailableProperty();
}

@Override
public boolean isRedoAvailable() {
return delegate.isRedoAvailable();
}

@Override
public ObservableBooleanValue performingActionProperty() {
return delegate.performingActionProperty();
}

@Override
public boolean isPerformingAction() {
return delegate.isPerformingAction();
}

@Override
public void preventMerge() {
delegate.preventMerge();
}

@Override
public void forgetHistory() {
delegate.forgetHistory();
}

@Override
public UndoPosition getCurrentPosition() {
return delegate.getCurrentPosition();
}

@Override
public void mark() {
delegate.mark();
}

@Override
public ObservableBooleanValue atMarkedPositionProperty() {
return delegate.atMarkedPositionProperty();
}

@Override
public boolean isAtMarkedPosition() {
return delegate.isAtMarkedPosition();
}

@Override
public void close() {
subscription.unsubscribe();
delegate.close();
}
}
86 changes: 78 additions & 8 deletions richtextfx/src/main/java/org/fxmisc/richtext/util/UndoUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import org.fxmisc.richtext.model.TextChange;
import org.fxmisc.undo.UndoManager;
import org.fxmisc.undo.UndoManagerFactory;
import org.reactfx.EventStream;

import java.time.Duration;
import java.util.function.Consumer;

/**
Expand All @@ -18,6 +20,8 @@ private UndoUtils() {
throw new IllegalStateException("UndoUtils cannot be instantiated");
}

public static final Duration DEFAULT_PREVENT_MERGE_DELAY = Duration.ofMillis(500);

/**
* Constructs an UndoManager with an unlimited history:
* if {@link GenericStyledArea#isPreserveStyle() the area's preserveStyle flag is true}, the returned UndoManager
Expand All @@ -38,33 +42,91 @@ public static <PS, SEG, S> UndoManager defaultUndoManager(GenericStyledArea<PS,
* ********************************************************************** */

/**
* Returns an UndoManager with an unlimited history that can undo/redo {@link RichTextChange}s.
* Returns an UndoManager with an unlimited history that can undo/redo {@link RichTextChange}s. New changes
* emitted from the stream will not be merged with the previous change
* after {@link #DEFAULT_PREVENT_MERGE_DELAY}
*/
public static <PS, SEG, S> UndoManager<RichTextChange<PS, SEG, S>> richTextUndoManager(GenericStyledArea<PS, SEG, S> area) {
return richTextUndoManager(area, UndoManagerFactory.unlimitedHistoryFactory());
}

/**
* Returns an UndoManager that can undo/redo {@link RichTextChange}s.
* Returns an UndoManager that can undo/redo {@link RichTextChange}s. New changes
* emitted from the stream will not be merged with the previous change
* after {@code preventMergeDelay}
*/
public static <PS, SEG, S> UndoManager<RichTextChange<PS, SEG, S>> richTextUndoManager(GenericStyledArea<PS, SEG, S> area,
UndoManagerFactory factory) {
return factory.create(area.richChanges(), TextChange::invert, applyRichTextChange(area), TextChange::mergeWith, TextChange::isIdentity);
Duration preventMergeDelay) {
return richTextUndoManager(area, UndoManagerFactory.unlimitedHistoryFactory(), preventMergeDelay);
};

/**
* Returns an UndoManager with an unlimited history that can undo/redo {@link PlainTextChange}s.
* Returns an UndoManager that can undo/redo {@link RichTextChange}s. New changes
* emitted from the stream will not be merged with the previous change
* after {@link #DEFAULT_PREVENT_MERGE_DELAY}
*/
public static <PS, SEG, S> UndoManager<RichTextChange<PS, SEG, S>> richTextUndoManager(GenericStyledArea<PS, SEG, S> area,
UndoManagerFactory factory) {
return richTextUndoManager(area, factory, DEFAULT_PREVENT_MERGE_DELAY);
};

/**
* Returns an UndoManager that can undo/redo {@link RichTextChange}s. New changes
* emitted from the stream will not be merged with the previous change
* after {@code preventMergeDelay}
*/
public static <PS, SEG, S> UndoManager<RichTextChange<PS, SEG, S>> richTextUndoManager(GenericStyledArea<PS, SEG, S> area,
UndoManagerFactory factory,
Duration preventMergeDelay) {
return wrap(
factory.create(area.richChanges(), TextChange::invert, applyRichTextChange(area), TextChange::mergeWith, TextChange::isIdentity),
area.richChanges(),
preventMergeDelay
);
};

/**
* Returns an UndoManager with an unlimited history that can undo/redo {@link PlainTextChange}s. New changes
* emitted from the stream will not be merged with the previous change
* after {@link #DEFAULT_PREVENT_MERGE_DELAY}
*/
public static <PS, SEG, S> UndoManager<PlainTextChange> plainTextUndoManager(GenericStyledArea<PS, SEG, S> area) {
return plainTextUndoManager(area, UndoManagerFactory.unlimitedHistoryFactory());
return plainTextUndoManager(area, DEFAULT_PREVENT_MERGE_DELAY);
}

/**
* Returns an UndoManager that can undo/redo {@link PlainTextChange}s.
* Returns an UndoManager that can undo/redo {@link PlainTextChange}s. New changes
* emitted from the stream will not be merged with the previous change
* after {@code preventMergeDelay}
*/
public static <PS, SEG, S> UndoManager<PlainTextChange> plainTextUndoManager(GenericStyledArea<PS, SEG, S> area,
Duration preventMergeDelay) {
return plainTextUndoManager(area, UndoManagerFactory.unlimitedHistoryFactory(), preventMergeDelay);
}

/**
* Returns an UndoManager that can undo/redo {@link PlainTextChange}s. New changes
* emitted from the stream will not be merged with the previous change
* after {@link #DEFAULT_PREVENT_MERGE_DELAY}
*/
public static <PS, SEG, S> UndoManager<PlainTextChange> plainTextUndoManager(GenericStyledArea<PS, SEG, S> area,
UndoManagerFactory factory) {
return factory.create(area.plainTextChanges(), TextChange::invert, applyPlainTextChange(area), TextChange::mergeWith, TextChange::isIdentity);
return plainTextUndoManager(area, factory, DEFAULT_PREVENT_MERGE_DELAY);
}

/**
* Returns an UndoManager that can undo/redo {@link PlainTextChange}s. New changes
* emitted from the stream will not be merged with the previous change
* after {@code preventMergeDelay}
*/
public static <PS, SEG, S> UndoManager<PlainTextChange> plainTextUndoManager(GenericStyledArea<PS, SEG, S> area,
UndoManagerFactory factory,
Duration preventMergeDelay) {
return wrap(
factory.create(area.plainTextChanges(), TextChange::invert, applyPlainTextChange(area), TextChange::mergeWith, TextChange::isIdentity),
area.plainTextChanges(),
preventMergeDelay
);
}

/* ********************************************************************** *
Expand All @@ -90,4 +152,12 @@ public static <PS, SEG, S> Consumer<PlainTextChange> applyPlainTextChange(Generi
public static <PS, SEG, S> Consumer<RichTextChange<PS, SEG, S>> applyRichTextChange(GenericStyledArea<PS, SEG, S> area) {
return change -> area.replace(change.getPosition(), change.getRemovalEnd(), change.getInserted());
}

/**
* Wraps an {@link UndoManager} and prevents the next emitted change from merging with the previous one are a
* period of inactivity (i.e., the {@code changeStream} has not emitted an event in {@code preventMergeDelay}
*/
public static <T> UndoManager<T> wrap(UndoManager<T> undoManager, EventStream<T> changeStream, Duration preventMergeDelay) {
return new UndoManagerInactivityWrapper<>(undoManager, changeStream, preventMergeDelay);
}
}

0 comments on commit 51b2306

Please sign in to comment.