Skip to content

Commit 66810ca

Browse files
Merge pull request #695 from JordanMartinez/multiTextChange
Allow multiple portions of an area's document to be updated in one call
2 parents 70509b5 + 9aaa6b9 commit 66810ca

14 files changed

Lines changed: 1035 additions & 161 deletions

richtextfx/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ if (gradle.gradleVersion.substring(0, 1) >= "4") {
2626
}
2727
dependencies {
2828
compile group: 'org.reactfx', name: 'reactfx', version: '2.0-M5'
29-
compile group: 'org.fxmisc.undo', name: 'undofx', version: '1.4.0'
29+
compile group: 'org.fxmisc.undo', name: 'undofx', version: '2.0.0'
3030
compile group: 'org.fxmisc.flowless', name: 'flowless', version: '0.6'
3131
compile group: 'org.fxmisc.wellbehaved', name: 'wellbehavedfx', version: '0.3.1'
3232

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package org.fxmisc.richtext.api;
2+
3+
import javafx.stage.Stage;
4+
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
5+
import org.fxmisc.richtext.MultiChangeBuilder;
6+
import org.junit.Test;
7+
8+
import static org.junit.Assert.assertEquals;
9+
import static org.junit.Assert.fail;
10+
11+
public class MultiChangeTest extends InlineCssTextAreaAppTest {
12+
13+
@Override
14+
public void start(Stage stage) throws Exception {
15+
super.start(stage);
16+
17+
// initialize area with some text
18+
area.replaceText("(text)");
19+
}
20+
21+
@Test
22+
public void committing_single_change_works() {
23+
interact(() -> {
24+
String text = area.getText();
25+
area.createMultiChange(1)
26+
.deleteText(0, 1)
27+
.commit();
28+
29+
assertEquals(text.substring(1), area.getText());
30+
});
31+
}
32+
33+
@Test
34+
public void committing_relative_change_works() {
35+
interact(() -> {
36+
String text = area.getText();
37+
String hello = "hello";
38+
String world = "world";
39+
area.createMultiChange(2)
40+
.insertText(0, hello)
41+
.insertText(0, world)
42+
.commit();
43+
44+
assertEquals(hello + world + text, area.getText());
45+
});
46+
}
47+
48+
@Test
49+
public void committing_absolute_change_works() {
50+
interact(() -> {
51+
String text = area.getText();
52+
String hello = "hello";
53+
String world = "world";
54+
area.createMultiChange(2)
55+
.insertText(0, hello)
56+
.insertTextAbsolutely(0, world)
57+
.commit();
58+
59+
assertEquals(world + hello + text, area.getText());
60+
});
61+
}
62+
63+
@Test
64+
public void changing_same_content_multiple_times_works() {
65+
interact(() -> {
66+
String text = area.getText();
67+
68+
area.createMultiChange(4)
69+
.replaceTextAbsolutely(0, 1, "a")
70+
.replaceTextAbsolutely(0, 1, "b")
71+
.replaceTextAbsolutely(0, 1, "c")
72+
.replaceTextAbsolutely(0, 1, "d")
73+
.commit();
74+
75+
assertEquals("d" + text.substring(1), area.getText());
76+
});
77+
}
78+
79+
@Test
80+
public void attempting_to_reuse_builder_throws_exception() {
81+
interact(() -> {
82+
MultiChangeBuilder<String, String, String> builder = area.createMultiChange(1)
83+
.insertText(0, "hey");
84+
builder.commit();
85+
try {
86+
builder.commit();
87+
fail();
88+
} catch (IllegalStateException e) {
89+
// cannot reuse builder once commit changes
90+
}
91+
});
92+
}
93+
94+
@Test
95+
public void attempting_to_commit_without_any_stored_changes_throws_exception() {
96+
interact(() -> {
97+
MultiChangeBuilder<String, String, String> builder = area.createMultiChange(1);
98+
try {
99+
builder.commit();
100+
fail();
101+
} catch (IllegalStateException e) {
102+
// no changes were stored in the builder
103+
}
104+
});
105+
}
106+
}

richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/UndoManagerTests.java

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package org.fxmisc.richtext.api;
22

33
import com.nitorcreations.junit.runners.NestedRunner;
4+
import javafx.beans.property.SimpleIntegerProperty;
45
import javafx.scene.Scene;
56
import javafx.scene.control.Label;
67
import javafx.stage.Stage;
78
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
89
import org.fxmisc.richtext.RichTextFXTestBase;
910
import org.fxmisc.richtext.StyledTextArea;
1011
import org.fxmisc.richtext.model.SimpleEditableStyledDocument;
12+
import org.fxmisc.richtext.model.TextChange;
1113
import org.fxmisc.richtext.util.UndoUtils;
1214
import org.junit.Test;
1315
import org.junit.runner.RunWith;
14-
import org.testfx.framework.junit.ApplicationTest;
1516

1617
import static org.junit.Assert.assertEquals;
18+
import static org.junit.Assert.assertFalse;
1719

1820
@RunWith(NestedRunner.class)
1921
public class UndoManagerTests {
@@ -54,6 +56,96 @@ public void testUndoWithWinNewlines() {
5456
});
5557
}
5658

59+
@Test
60+
public void multiChange_undo_and_redo_works() {
61+
interact(() -> {
62+
String text = "text";
63+
String wrappedText = "(" + text + ")";
64+
area.replaceText(wrappedText);
65+
area.getUndoManager().forgetHistory();
66+
67+
// Text: |(|t|e|x|t|)|
68+
// Position: 0 1 2 3 4 5 6
69+
area.createMultiChange(2)
70+
// delete parenthesis
71+
.deleteText(0, 1)
72+
.deleteText(5, 6)
73+
.commit();
74+
75+
area.undo();
76+
assertEquals(wrappedText, area.getText());
77+
78+
area.redo();
79+
assertEquals(text, area.getText());
80+
});
81+
}
82+
83+
@Test
84+
public void multiChange_merge_works() {
85+
interact(() -> {
86+
String initialText = "123456";
87+
area.replaceText(initialText);
88+
area.getUndoManager().forgetHistory();
89+
90+
int firstCount = 0;
91+
int secondCount = 3;
92+
93+
// Text: |1|2|3|4|5|6|
94+
// Position: 0 1 2 3 4 5 6
95+
area.createMultiChange(2)
96+
// replace '1' with 'a'
97+
.replaceText(firstCount, ++firstCount, "a")
98+
// replace '4' with 'c'
99+
.replaceText(secondCount, ++secondCount, "c")
100+
.commit();
101+
102+
// Text: |a|2|3|c|5|6|
103+
// Position: 0 1 2 3 4 5 6
104+
area.createMultiChange(2)
105+
// replace '2' with 'b'
106+
.replaceText(firstCount, ++firstCount, "b")
107+
// replace '5' with 'd'
108+
.replaceText(secondCount, ++secondCount, "d")
109+
.commit();
110+
111+
String finalText = "ab3cd6";
112+
113+
area.undo();
114+
assertFalse(area.getUndoManager().isUndoAvailable());
115+
assertEquals(initialText, area.getText());
116+
117+
area.redo();
118+
assertFalse(area.getUndoManager().isRedoAvailable());
119+
assertEquals(finalText, area.getText());
120+
});
121+
}
122+
123+
@Test
124+
public void identity_change_works() {
125+
interact(() -> {
126+
area.replaceText("ttttt");
127+
128+
SimpleIntegerProperty richEmissions = new SimpleIntegerProperty(0);
129+
SimpleIntegerProperty plainEmissions = new SimpleIntegerProperty(0);
130+
area.multiRichChanges()
131+
.hook(list -> richEmissions.set(richEmissions.get() + 1))
132+
.filter(list -> !list.stream().allMatch(TextChange::isIdentity))
133+
.subscribe(list -> plainEmissions.set(plainEmissions.get() + 1));
134+
135+
136+
int position = 0;
137+
area.createMultiChange(4)
138+
.replaceText(position, ++position, "t")
139+
.replaceText(position, ++position, "t")
140+
.replaceText(position, ++position, "t")
141+
.replaceText(position, ++position, "t")
142+
.commit();
143+
144+
assertEquals(1, richEmissions.get());
145+
assertEquals(0, plainEmissions.get());
146+
});
147+
}
148+
57149
}
58150

59151
public class UsingStyledTextArea extends RichTextFXTestBase {

richtextfx/src/main/java/org/fxmisc/richtext/CaretNode.java

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import javafx.css.StyleableObjectProperty;
99
import javafx.geometry.Bounds;
1010
import javafx.scene.shape.Path;
11+
import org.fxmisc.richtext.model.PlainTextChange;
1112
import org.fxmisc.richtext.model.TwoDimensional;
1213
import org.reactfx.EventStream;
1314
import org.reactfx.EventStreams;
@@ -155,25 +156,28 @@ public CaretNode(String name, GenericStyledArea<?, ?, ?> area, SuspendableNo dep
155156

156157
// when content is updated by an area, update the caret of all the other
157158
// clones that also display the same document
158-
manageSubscription(area.plainTextChanges(), (plainTextChange -> {
159-
int netLength = plainTextChange.getNetLength();
160-
if (netLength != 0) {
161-
int indexOfChange = plainTextChange.getPosition();
162-
// in case of a replacement: "hello there" -> "hi."
163-
int endOfChange = indexOfChange + Math.abs(netLength);
164-
165-
int caretPosition = getPosition();
166-
if (indexOfChange < caretPosition) {
167-
// if caret is within the changed content, move it to indexOfChange
168-
// otherwise offset it by netLength
169-
moveTo(
170-
caretPosition < endOfChange
171-
? indexOfChange
172-
: caretPosition + netLength
173-
);
159+
manageSubscription(area.multiPlainChanges(), list -> {
160+
int finalPosition = getPosition();
161+
for (PlainTextChange plainTextChange : list) {
162+
int netLength = plainTextChange.getNetLength();
163+
if (netLength != 0) {
164+
int indexOfChange = plainTextChange.getPosition();
165+
// in case of a replacement: "hello there" -> "hi."
166+
int endOfChange = indexOfChange + Math.abs(netLength);
167+
168+
if (indexOfChange < finalPosition) {
169+
// if caret is within the changed content, move it to indexOfChange
170+
// otherwise offset it by netLength
171+
finalPosition = finalPosition < endOfChange
172+
? indexOfChange
173+
: finalPosition + netLength;
174+
}
174175
}
175176
}
176-
}));
177+
if (finalPosition != getPosition()) {
178+
moveTo(finalPosition);
179+
}
180+
});
177181

178182
// whether or not to display the caret
179183
EventStream<Boolean> blinkCaret = showCaret.values()

richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.fxmisc.richtext.model.Paragraph;
6161
import org.fxmisc.richtext.model.ReadOnlyStyledDocument;
6262
import org.fxmisc.richtext.model.PlainTextChange;
63+
import org.fxmisc.richtext.model.Replacement;
6364
import org.fxmisc.richtext.model.RichTextChange;
6465
import org.fxmisc.richtext.model.StyleSpans;
6566
import org.fxmisc.richtext.model.StyledDocument;
@@ -252,6 +253,9 @@
252253
* within this area (e.g. properties and their getters/setters, etc.), look at the interfaces
253254
* this area implements. Each lists and documents methods that fall under that category.
254255
* </p>
256+
* <p>
257+
* To update multiple portions of the area's underlying document in one call, see {@link #createMultiChange()}.
258+
* </p>
255259
*
256260
* <h3>Calculating a Position Within the Area</h3>
257261
* <p>
@@ -526,6 +530,10 @@ public final boolean removeSelection(Selection<PS, SEG, S> selection) {
526530
* *
527531
* ********************************************************************** */
528532

533+
@Override public EventStream<List<RichTextChange<PS, SEG, S>>> multiRichChanges() { return content.multiRichChanges(); }
534+
535+
@Override public EventStream<List<PlainTextChange>> multiPlainChanges() { return content.multiPlainChanges(); }
536+
529537
// text changes
530538
@Override public final EventStream<PlainTextChange> plainTextChanges() { return content.plainChanges(); }
531539

@@ -1190,6 +1198,23 @@ public void replace(int start, int end, StyledDocument<PS, SEG, S> replacement)
11901198
selectRange(newCaretPos, newCaretPos);
11911199
}
11921200

1201+
void replaceMulti(List<Replacement<PS, SEG, S>> replacements) {
1202+
content.replaceMulti(replacements);
1203+
1204+
// don't update selection as this is not the main method through which the area is updated
1205+
// leave that up to the developer using it to determine what to do
1206+
}
1207+
1208+
@Override
1209+
public MultiChangeBuilder<PS, SEG, S> createMultiChange() {
1210+
return new MultiChangeBuilder<>(this);
1211+
}
1212+
1213+
@Override
1214+
public MultiChangeBuilder<PS, SEG, S> createMultiChange(int initialNumOfChanges) {
1215+
return new MultiChangeBuilder<>(this, initialNumOfChanges);
1216+
}
1217+
11931218
/* ********************************************************************** *
11941219
* *
11951220
* Public API *

0 commit comments

Comments
 (0)