Skip to content
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
2 changes: 1 addition & 1 deletion richtextfx/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ if (gradle.gradleVersion.substring(0, 1) >= "4") {
}
dependencies {
compile group: 'org.reactfx', name: 'reactfx', version: '2.0-M5'
compile group: 'org.fxmisc.undo', name: 'undofx', version: '1.4.0'
compile group: 'org.fxmisc.undo', name: 'undofx', version: '2.0.0'
compile group: 'org.fxmisc.flowless', name: 'flowless', version: '0.6'
compile group: 'org.fxmisc.wellbehaved', name: 'wellbehavedfx', version: '0.3.1'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.fxmisc.richtext.api;

import javafx.stage.Stage;
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
import org.fxmisc.richtext.MultiChangeBuilder;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

public class MultiChangeTest extends InlineCssTextAreaAppTest {

@Override
public void start(Stage stage) throws Exception {
super.start(stage);

// initialize area with some text
area.replaceText("(text)");
}

@Test
public void committing_single_change_works() {
interact(() -> {
String text = area.getText();
area.createMultiChange(1)
.deleteText(0, 1)
.commit();

assertEquals(text.substring(1), area.getText());
});
}

@Test
public void committing_relative_change_works() {
interact(() -> {
String text = area.getText();
String hello = "hello";
String world = "world";
area.createMultiChange(2)
.insertText(0, hello)
.insertText(0, world)
.commit();

assertEquals(hello + world + text, area.getText());
});
}

@Test
public void committing_absolute_change_works() {
interact(() -> {
String text = area.getText();
String hello = "hello";
String world = "world";
area.createMultiChange(2)
.insertText(0, hello)
.insertTextAbsolutely(0, world)
.commit();

assertEquals(world + hello + text, area.getText());
});
}

@Test
public void changing_same_content_multiple_times_works() {
interact(() -> {
String text = area.getText();

area.createMultiChange(4)
.replaceTextAbsolutely(0, 1, "a")
.replaceTextAbsolutely(0, 1, "b")
.replaceTextAbsolutely(0, 1, "c")
.replaceTextAbsolutely(0, 1, "d")
.commit();

assertEquals("d" + text.substring(1), area.getText());
});
}

@Test
public void attempting_to_reuse_builder_throws_exception() {
interact(() -> {
MultiChangeBuilder<String, String, String> builder = area.createMultiChange(1)
.insertText(0, "hey");
builder.commit();
try {
builder.commit();
fail();
} catch (IllegalStateException e) {
// cannot reuse builder once commit changes
}
});
}

@Test
public void attempting_to_commit_without_any_stored_changes_throws_exception() {
interact(() -> {
MultiChangeBuilder<String, String, String> builder = area.createMultiChange(1);
try {
builder.commit();
fail();
} catch (IllegalStateException e) {
// no changes were stored in the builder
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package org.fxmisc.richtext.api;

import com.nitorcreations.junit.runners.NestedRunner;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
import org.fxmisc.richtext.RichTextFXTestBase;
import org.fxmisc.richtext.StyledTextArea;
import org.fxmisc.richtext.model.SimpleEditableStyledDocument;
import org.fxmisc.richtext.model.TextChange;
import org.fxmisc.richtext.util.UndoUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.testfx.framework.junit.ApplicationTest;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;

@RunWith(NestedRunner.class)
public class UndoManagerTests {
Expand Down Expand Up @@ -54,6 +56,96 @@ public void testUndoWithWinNewlines() {
});
}

@Test
public void multiChange_undo_and_redo_works() {
interact(() -> {
String text = "text";
String wrappedText = "(" + text + ")";
area.replaceText(wrappedText);
area.getUndoManager().forgetHistory();

// Text: |(|t|e|x|t|)|
// Position: 0 1 2 3 4 5 6
area.createMultiChange(2)
// delete parenthesis
.deleteText(0, 1)
.deleteText(5, 6)
.commit();

area.undo();
assertEquals(wrappedText, area.getText());

area.redo();
assertEquals(text, area.getText());
});
}

@Test
public void multiChange_merge_works() {
interact(() -> {
String initialText = "123456";
area.replaceText(initialText);
area.getUndoManager().forgetHistory();

int firstCount = 0;
int secondCount = 3;

// Text: |1|2|3|4|5|6|
// Position: 0 1 2 3 4 5 6
area.createMultiChange(2)
// replace '1' with 'a'
.replaceText(firstCount, ++firstCount, "a")
// replace '4' with 'c'
.replaceText(secondCount, ++secondCount, "c")
.commit();

// Text: |a|2|3|c|5|6|
// Position: 0 1 2 3 4 5 6
area.createMultiChange(2)
// replace '2' with 'b'
.replaceText(firstCount, ++firstCount, "b")
// replace '5' with 'd'
.replaceText(secondCount, ++secondCount, "d")
.commit();

String finalText = "ab3cd6";

area.undo();
assertFalse(area.getUndoManager().isUndoAvailable());
assertEquals(initialText, area.getText());

area.redo();
assertFalse(area.getUndoManager().isRedoAvailable());
assertEquals(finalText, area.getText());
});
}

@Test
public void identity_change_works() {
interact(() -> {
area.replaceText("ttttt");

SimpleIntegerProperty richEmissions = new SimpleIntegerProperty(0);
SimpleIntegerProperty plainEmissions = new SimpleIntegerProperty(0);
area.multiRichChanges()
.hook(list -> richEmissions.set(richEmissions.get() + 1))
.filter(list -> !list.stream().allMatch(TextChange::isIdentity))
.subscribe(list -> plainEmissions.set(plainEmissions.get() + 1));


int position = 0;
area.createMultiChange(4)
.replaceText(position, ++position, "t")
.replaceText(position, ++position, "t")
.replaceText(position, ++position, "t")
.replaceText(position, ++position, "t")
.commit();

assertEquals(1, richEmissions.get());
assertEquals(0, plainEmissions.get());
});
}

}

public class UsingStyledTextArea extends RichTextFXTestBase {
Expand Down
38 changes: 21 additions & 17 deletions richtextfx/src/main/java/org/fxmisc/richtext/CaretNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import javafx.css.StyleableObjectProperty;
import javafx.geometry.Bounds;
import javafx.scene.shape.Path;
import org.fxmisc.richtext.model.PlainTextChange;
import org.fxmisc.richtext.model.TwoDimensional;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
Expand Down Expand Up @@ -155,25 +156,28 @@ public CaretNode(String name, GenericStyledArea<?, ?, ?> area, SuspendableNo dep

// when content is updated by an area, update the caret of all the other
// clones that also display the same document
manageSubscription(area.plainTextChanges(), (plainTextChange -> {
int netLength = plainTextChange.getNetLength();
if (netLength != 0) {
int indexOfChange = plainTextChange.getPosition();
// in case of a replacement: "hello there" -> "hi."
int endOfChange = indexOfChange + Math.abs(netLength);

int caretPosition = getPosition();
if (indexOfChange < caretPosition) {
// if caret is within the changed content, move it to indexOfChange
// otherwise offset it by netLength
moveTo(
caretPosition < endOfChange
? indexOfChange
: caretPosition + netLength
);
manageSubscription(area.multiPlainChanges(), list -> {
int finalPosition = getPosition();
for (PlainTextChange plainTextChange : list) {
int netLength = plainTextChange.getNetLength();
if (netLength != 0) {
int indexOfChange = plainTextChange.getPosition();
// in case of a replacement: "hello there" -> "hi."
int endOfChange = indexOfChange + Math.abs(netLength);

if (indexOfChange < finalPosition) {
// if caret is within the changed content, move it to indexOfChange
// otherwise offset it by netLength
finalPosition = finalPosition < endOfChange
? indexOfChange
: finalPosition + netLength;
}
}
}
}));
if (finalPosition != getPosition()) {
moveTo(finalPosition);
}
});

// whether or not to display the caret
EventStream<Boolean> blinkCaret = showCaret.values()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.fxmisc.richtext.model.Paragraph;
import org.fxmisc.richtext.model.ReadOnlyStyledDocument;
import org.fxmisc.richtext.model.PlainTextChange;
import org.fxmisc.richtext.model.Replacement;
import org.fxmisc.richtext.model.RichTextChange;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyledDocument;
Expand Down Expand Up @@ -252,6 +253,9 @@
* within this area (e.g. properties and their getters/setters, etc.), look at the interfaces
* this area implements. Each lists and documents methods that fall under that category.
* </p>
* <p>
* To update multiple portions of the area's underlying document in one call, see {@link #createMultiChange()}.
* </p>
*
* <h3>Calculating a Position Within the Area</h3>
* <p>
Expand Down Expand Up @@ -526,6 +530,10 @@ public final boolean removeSelection(Selection<PS, SEG, S> selection) {
* *
* ********************************************************************** */

@Override public EventStream<List<RichTextChange<PS, SEG, S>>> multiRichChanges() { return content.multiRichChanges(); }

@Override public EventStream<List<PlainTextChange>> multiPlainChanges() { return content.multiPlainChanges(); }

// text changes
@Override public final EventStream<PlainTextChange> plainTextChanges() { return content.plainChanges(); }

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

void replaceMulti(List<Replacement<PS, SEG, S>> replacements) {
content.replaceMulti(replacements);

// don't update selection as this is not the main method through which the area is updated
// leave that up to the developer using it to determine what to do
}

@Override
public MultiChangeBuilder<PS, SEG, S> createMultiChange() {
return new MultiChangeBuilder<>(this);
}

@Override
public MultiChangeBuilder<PS, SEG, S> createMultiChange(int initialNumOfChanges) {
return new MultiChangeBuilder<>(this, initialNumOfChanges);
}

/* ********************************************************************** *
* *
* Public API *
Expand Down
Loading