Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop next change from merging with previous one after inactive period. #530

Merged
merged 2 commits into from
Jun 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}