Skip to content

Commit 51b2306

Browse files
Merge pull request #530 from JordanMartinez/preventUndoMergeOnInactivity
Stop next change from merging with previous one after inactive period.
2 parents c73aacb + 1b90a9a commit 51b2306

4 files changed

Lines changed: 236 additions & 8 deletions

File tree

richtextfx/src/integrationTest/java/org/fxmisc/richtext/view/MiscellaneousAPITests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.fxmisc.richtext.Caret;
1010
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
1111
import org.fxmisc.richtext.model.NavigationActions;
12+
import org.fxmisc.richtext.util.UndoUtils;
1213
import org.junit.Before;
1314
import org.junit.Test;
1415
import org.junit.runner.RunWith;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.fxmisc.richtext.view;
2+
3+
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
4+
import org.fxmisc.richtext.util.UndoUtils;
5+
import org.junit.Test;
6+
7+
import static org.junit.Assert.assertEquals;
8+
9+
public class UndoManagerTests extends InlineCssTextAreaAppTest {
10+
11+
@Test
12+
public void preventMergeOfIncomingChangeAfterPeriodOfUserInactivity() {
13+
String text1 = "text1";
14+
String text2 = "text2";
15+
16+
long periodOfUserInactivity = UndoUtils.DEFAULT_PREVENT_MERGE_DELAY.toMillis() + 300L;
17+
18+
write(text1);
19+
sleep(periodOfUserInactivity);
20+
write(text2);
21+
22+
interact(area::undo);
23+
assertEquals(text1, area.getText());
24+
25+
interact(area::undo);
26+
assertEquals("", area.getText());
27+
}
28+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package org.fxmisc.richtext.util;
2+
3+
import javafx.beans.value.ObservableBooleanValue;
4+
import org.fxmisc.undo.UndoManager;
5+
import org.reactfx.EventStream;
6+
import org.reactfx.Subscription;
7+
import org.reactfx.value.Val;
8+
9+
import java.time.Duration;
10+
11+
/**
12+
* A wrapper around an {@link UndoManager} that prevents the next emitted change from merging with the previous
13+
* one after a period of inactivity (i.e., the UndoManager's {@code changeSource} has not emitted an event
14+
* after a specified period of time.
15+
*
16+
* @param <C> the type of change the UndoManager can undo/redo
17+
*/
18+
final class UndoManagerInactivityWrapper<C> implements UndoManager<C> {
19+
20+
private final UndoManager<C> delegate;
21+
private final Subscription subscription;
22+
23+
/**
24+
* Wraps an {@link UndoManager} and prevents the next emitted change from merging with the previous one
25+
* after a period of inactivity (i.e., the {@code changeSource} has not emitted an event for
26+
* {@code preventMergeDelay}). <b>Note:</b> there is no check that insures that the {@code changeSource}
27+
* parameter is the same one used by the {@code undoManager} parameter
28+
*/
29+
public UndoManagerInactivityWrapper(UndoManager<C> undoManager, EventStream<C> changeSource, Duration preventMergeDelay) {
30+
this.delegate = undoManager;
31+
subscription = changeSource.successionEnds(preventMergeDelay).subscribe(ignore -> preventMerge());
32+
}
33+
34+
@Override
35+
public boolean undo() {
36+
return delegate.undo();
37+
}
38+
39+
@Override
40+
public boolean redo() {
41+
return delegate.redo();
42+
}
43+
44+
@Override
45+
public Val<Boolean> undoAvailableProperty() {
46+
return delegate.undoAvailableProperty();
47+
}
48+
49+
@Override
50+
public boolean isUndoAvailable() {
51+
return delegate.isUndoAvailable();
52+
}
53+
54+
@Override
55+
public Val<C> nextToUndoProperty() {
56+
return delegate.nextToUndoProperty();
57+
}
58+
59+
@Override
60+
public C getNextToUndo() {
61+
return delegate.getNextToUndo();
62+
}
63+
64+
@Override
65+
public Val<C> nextToRedoProperty() {
66+
return delegate.nextToRedoProperty();
67+
}
68+
69+
@Override
70+
public C getNextToRedo() {
71+
return delegate.getNextToRedo();
72+
}
73+
74+
@Override
75+
public Val<Boolean> redoAvailableProperty() {
76+
return delegate.redoAvailableProperty();
77+
}
78+
79+
@Override
80+
public boolean isRedoAvailable() {
81+
return delegate.isRedoAvailable();
82+
}
83+
84+
@Override
85+
public ObservableBooleanValue performingActionProperty() {
86+
return delegate.performingActionProperty();
87+
}
88+
89+
@Override
90+
public boolean isPerformingAction() {
91+
return delegate.isPerformingAction();
92+
}
93+
94+
@Override
95+
public void preventMerge() {
96+
delegate.preventMerge();
97+
}
98+
99+
@Override
100+
public void forgetHistory() {
101+
delegate.forgetHistory();
102+
}
103+
104+
@Override
105+
public UndoPosition getCurrentPosition() {
106+
return delegate.getCurrentPosition();
107+
}
108+
109+
@Override
110+
public void mark() {
111+
delegate.mark();
112+
}
113+
114+
@Override
115+
public ObservableBooleanValue atMarkedPositionProperty() {
116+
return delegate.atMarkedPositionProperty();
117+
}
118+
119+
@Override
120+
public boolean isAtMarkedPosition() {
121+
return delegate.isAtMarkedPosition();
122+
}
123+
124+
@Override
125+
public void close() {
126+
subscription.unsubscribe();
127+
delegate.close();
128+
}
129+
}

richtextfx/src/main/java/org/fxmisc/richtext/util/UndoUtils.java

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import org.fxmisc.richtext.model.TextChange;
77
import org.fxmisc.undo.UndoManager;
88
import org.fxmisc.undo.UndoManagerFactory;
9+
import org.reactfx.EventStream;
910

11+
import java.time.Duration;
1012
import java.util.function.Consumer;
1113

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

23+
public static final Duration DEFAULT_PREVENT_MERGE_DELAY = Duration.ofMillis(500);
24+
2125
/**
2226
* Constructs an UndoManager with an unlimited history:
2327
* if {@link GenericStyledArea#isPreserveStyle() the area's preserveStyle flag is true}, the returned UndoManager
@@ -38,33 +42,91 @@ public static <PS, SEG, S> UndoManager defaultUndoManager(GenericStyledArea<PS,
3842
* ********************************************************************** */
3943

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

4753
/**
48-
* Returns an UndoManager that can undo/redo {@link RichTextChange}s.
54+
* Returns an UndoManager that can undo/redo {@link RichTextChange}s. New changes
55+
* emitted from the stream will not be merged with the previous change
56+
* after {@code preventMergeDelay}
4957
*/
5058
public static <PS, SEG, S> UndoManager<RichTextChange<PS, SEG, S>> richTextUndoManager(GenericStyledArea<PS, SEG, S> area,
51-
UndoManagerFactory factory) {
52-
return factory.create(area.richChanges(), TextChange::invert, applyRichTextChange(area), TextChange::mergeWith, TextChange::isIdentity);
59+
Duration preventMergeDelay) {
60+
return richTextUndoManager(area, UndoManagerFactory.unlimitedHistoryFactory(), preventMergeDelay);
5361
};
5462

5563
/**
56-
* Returns an UndoManager with an unlimited history that can undo/redo {@link PlainTextChange}s.
64+
* Returns an UndoManager that can undo/redo {@link RichTextChange}s. New changes
65+
* emitted from the stream will not be merged with the previous change
66+
* after {@link #DEFAULT_PREVENT_MERGE_DELAY}
67+
*/
68+
public static <PS, SEG, S> UndoManager<RichTextChange<PS, SEG, S>> richTextUndoManager(GenericStyledArea<PS, SEG, S> area,
69+
UndoManagerFactory factory) {
70+
return richTextUndoManager(area, factory, DEFAULT_PREVENT_MERGE_DELAY);
71+
};
72+
73+
/**
74+
* Returns an UndoManager that can undo/redo {@link RichTextChange}s. New changes
75+
* emitted from the stream will not be merged with the previous change
76+
* after {@code preventMergeDelay}
77+
*/
78+
public static <PS, SEG, S> UndoManager<RichTextChange<PS, SEG, S>> richTextUndoManager(GenericStyledArea<PS, SEG, S> area,
79+
UndoManagerFactory factory,
80+
Duration preventMergeDelay) {
81+
return wrap(
82+
factory.create(area.richChanges(), TextChange::invert, applyRichTextChange(area), TextChange::mergeWith, TextChange::isIdentity),
83+
area.richChanges(),
84+
preventMergeDelay
85+
);
86+
};
87+
88+
/**
89+
* Returns an UndoManager with an unlimited history that can undo/redo {@link PlainTextChange}s. New changes
90+
* emitted from the stream will not be merged with the previous change
91+
* after {@link #DEFAULT_PREVENT_MERGE_DELAY}
5792
*/
5893
public static <PS, SEG, S> UndoManager<PlainTextChange> plainTextUndoManager(GenericStyledArea<PS, SEG, S> area) {
59-
return plainTextUndoManager(area, UndoManagerFactory.unlimitedHistoryFactory());
94+
return plainTextUndoManager(area, DEFAULT_PREVENT_MERGE_DELAY);
6095
}
6196

6297
/**
63-
* Returns an UndoManager that can undo/redo {@link PlainTextChange}s.
98+
* Returns an UndoManager that can undo/redo {@link PlainTextChange}s. New changes
99+
* emitted from the stream will not be merged with the previous change
100+
* after {@code preventMergeDelay}
101+
*/
102+
public static <PS, SEG, S> UndoManager<PlainTextChange> plainTextUndoManager(GenericStyledArea<PS, SEG, S> area,
103+
Duration preventMergeDelay) {
104+
return plainTextUndoManager(area, UndoManagerFactory.unlimitedHistoryFactory(), preventMergeDelay);
105+
}
106+
107+
/**
108+
* Returns an UndoManager that can undo/redo {@link PlainTextChange}s. New changes
109+
* emitted from the stream will not be merged with the previous change
110+
* after {@link #DEFAULT_PREVENT_MERGE_DELAY}
64111
*/
65112
public static <PS, SEG, S> UndoManager<PlainTextChange> plainTextUndoManager(GenericStyledArea<PS, SEG, S> area,
66113
UndoManagerFactory factory) {
67-
return factory.create(area.plainTextChanges(), TextChange::invert, applyPlainTextChange(area), TextChange::mergeWith, TextChange::isIdentity);
114+
return plainTextUndoManager(area, factory, DEFAULT_PREVENT_MERGE_DELAY);
115+
}
116+
117+
/**
118+
* Returns an UndoManager that can undo/redo {@link PlainTextChange}s. New changes
119+
* emitted from the stream will not be merged with the previous change
120+
* after {@code preventMergeDelay}
121+
*/
122+
public static <PS, SEG, S> UndoManager<PlainTextChange> plainTextUndoManager(GenericStyledArea<PS, SEG, S> area,
123+
UndoManagerFactory factory,
124+
Duration preventMergeDelay) {
125+
return wrap(
126+
factory.create(area.plainTextChanges(), TextChange::invert, applyPlainTextChange(area), TextChange::mergeWith, TextChange::isIdentity),
127+
area.plainTextChanges(),
128+
preventMergeDelay
129+
);
68130
}
69131

70132
/* ********************************************************************** *
@@ -90,4 +152,12 @@ public static <PS, SEG, S> Consumer<PlainTextChange> applyPlainTextChange(Generi
90152
public static <PS, SEG, S> Consumer<RichTextChange<PS, SEG, S>> applyRichTextChange(GenericStyledArea<PS, SEG, S> area) {
91153
return change -> area.replace(change.getPosition(), change.getRemovalEnd(), change.getInserted());
92154
}
155+
156+
/**
157+
* Wraps an {@link UndoManager} and prevents the next emitted change from merging with the previous one are a
158+
* period of inactivity (i.e., the {@code changeStream} has not emitted an event in {@code preventMergeDelay}
159+
*/
160+
public static <T> UndoManager<T> wrap(UndoManager<T> undoManager, EventStream<T> changeStream, Duration preventMergeDelay) {
161+
return new UndoManagerInactivityWrapper<>(undoManager, changeStream, preventMergeDelay);
162+
}
93163
}

0 commit comments

Comments
 (0)