Skip to content

Commit 3a20f59

Browse files
Merge pull request #687 from JordanMartinez/multiCaretSelection
Allow area to display multiple carets and selections
2 parents 3fae2c8 + b59a1a8 commit 3a20f59

22 files changed

Lines changed: 1546 additions & 336 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package org.fxmisc.richtext.demo;
2+
3+
import javafx.application.Application;
4+
import javafx.scene.Scene;
5+
import javafx.scene.paint.Color;
6+
import javafx.stage.Stage;
7+
import javafx.util.Duration;
8+
import org.fxmisc.richtext.CaretNode;
9+
import org.fxmisc.richtext.InlineCssTextArea;
10+
import org.fxmisc.richtext.Selection;
11+
import org.fxmisc.richtext.SelectionImpl;
12+
13+
public class MultiCaretAndSelectionDemo extends Application {
14+
15+
private InlineCssTextArea area;
16+
17+
@Override
18+
public void start(Stage primaryStage) {
19+
// initialize area with some lines of text
20+
String alphabet = "abcdefghijklmnopqrstuvwxyz";
21+
StringBuilder sb = new StringBuilder();
22+
for (int i = 0; i < 10; i++) {
23+
sb.append(i).append(" :").append(alphabet).append("\n");
24+
}
25+
area = new InlineCssTextArea(sb.toString());
26+
27+
setupRTFXSpecificCSSShapes();
28+
29+
addExtraCaret();
30+
31+
addExtraSelection();
32+
33+
// select some other range with the regular caret/selection before showing area
34+
area.selectRange(2, 0, 2, 4);
35+
36+
primaryStage.setScene(new Scene(area, 400, 400));
37+
primaryStage.show();
38+
39+
// request focus so carets blink
40+
area.requestFocus();
41+
}
42+
43+
private void addExtraCaret() {
44+
CaretNode extraCaret = new CaretNode("another caret", area);
45+
if (!area.addCaret(extraCaret)) {
46+
throw new IllegalStateException("caret was not added to area");
47+
}
48+
extraCaret.moveTo(3, 8);
49+
50+
// since the CSS properties are re-set when it applies the CSS from files
51+
// remove the style class so that properties set below are not overridden by CSS
52+
extraCaret.getStyleClass().remove("caret");
53+
54+
extraCaret.setStrokeWidth(10.0);
55+
extraCaret.setStroke(Color.BROWN);
56+
extraCaret.setBlinkRate(Duration.millis(200));
57+
}
58+
59+
private void addExtraSelection() {
60+
Selection<String, String, String> extraSelection = new SelectionImpl<>("another selection", area,
61+
path -> {
62+
// make rendered selection path look like a yellow highlighter
63+
path.setStrokeWidth(0);
64+
path.setFill(Color.YELLOW);
65+
}
66+
);
67+
if (!area.addSelection(extraSelection)) {
68+
throw new IllegalStateException("selection was not added to area");
69+
}
70+
// select something so it is visible
71+
extraSelection.selectRange(7, 2, 7, 8);
72+
}
73+
74+
/**
75+
* Shows that RTFX-specific-CSS shapes are laid out in correct order, so that
76+
* selection and text and caret appears on top of them when made/moved
77+
*/
78+
private void setupRTFXSpecificCSSShapes() {
79+
String background = "-rtfx-background-color: red; ";
80+
String underline = "-rtfx-underline-color: blue; " +
81+
"-rtfx-underline-width: 2.0;";
82+
String border = "-rtfx-border-stroke-color: green; " +
83+
"-rtfx-border-stroke-width: 3.0;";
84+
85+
// set all of them at once on a give line to insure they display properly
86+
area.setStyle(0, background + underline + border);
87+
88+
// set each one over a given segment
89+
area.setStyle(1, 0, 3, background);
90+
area.setStyle(1, 4, 7, underline);
91+
area.setStyle(1, 8, 11, border);
92+
}
93+
}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@ public void start(Stage stage) throws Exception {
1616
}
1717

1818
@Test
19-
public void selection_bounds_are_unchanged_when_call_getCharacterBounds() {
19+
public void getCharacterBounds_works_even_when_a_selection_is_made() {
2020
area.selectAll();
2121
Bounds bounds = area.getSelectionBounds().get();
2222

23-
// getCharacterBoundsOnScreen() uses the selection shape to calculate the bounds
24-
// so insure it doesn't affect the selection shape if something is selected
25-
// before it gets called
26-
area.getCharacterBoundsOnScreen(0, area.getLength() - 1);
23+
interact(() -> area.getCharacterBoundsOnScreen(0, area.getLength() - 1));
2724

2825
assertEquals(bounds, area.getSelectionBounds().get());
2926
}

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

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
import org.junit.Test;
1313
import org.junit.runner.RunWith;
1414

15+
import java.util.concurrent.ExecutionException;
1516
import java.util.function.Consumer;
1617

1718
import static javafx.scene.input.MouseButton.PRIMARY;
1819
import static org.junit.Assert.assertEquals;
20+
import static org.testfx.util.WaitForAsyncUtils.asyncFx;
1921

2022
@RunWith(NestedRunner.class)
2123
public class HitTests extends InlineCssTextAreaAppTest {
@@ -64,13 +66,14 @@ public void setup() {
6466

6567
@Test
6668
public void clicking_in_top_padding_moves_caret_to_top_line() {
67-
interact(() -> area.setPadding(new Insets(PADDING_AMOUNT, 0, 0, 0)));
68-
69-
moveCaretToAreaEnd();
69+
interact(() -> {
70+
area.setPadding(new Insets(PADDING_AMOUNT, 0, 0, 0));
71+
moveCaretToAreaEnd();
72+
});
7073
moveTo(position(Pos.TOP_LEFT, 1, 2)).clickOn(PRIMARY);
7174
assertEquals(0, area.getCurrentParagraph());
7275

73-
moveCaretToAreaEnd();
76+
interact(() -> moveCaretToAreaEnd());
7477
moveTo(position(Pos.TOP_CENTER, 0, 0)).clickOn(PRIMARY);
7578
assertEquals(0, area.getCurrentParagraph());
7679
}
@@ -126,9 +129,12 @@ public void setup() {
126129
}
127130

128131
@Test
129-
public void clicking_character_should_move_caret_to_that_position() {
132+
public void clicking_character_should_move_caret_to_that_position()
133+
throws InterruptedException, ExecutionException {
130134
int start = area.getAbsolutePosition(3, 8);
131-
Bounds b = area.getCharacterBoundsOnScreen(start, start + 1).get();
135+
Bounds b = asyncFx(
136+
() -> area.getCharacterBoundsOnScreen(start, start + 1).get())
137+
.get();
132138
moveTo(b).clickOn(PRIMARY);
133139
assertEquals(start, area.getCaretPosition());
134140
}
@@ -151,9 +157,9 @@ public void prev_page_moves_caret_to_top_of_page() {
151157
@Test
152158
public void next_page_moves_caret_to_bottom_of_page() {
153159
area.showParagraphAtTop(0);
154-
area.moveTo(0);
155160

156161
interact(() -> {
162+
area.moveTo(0);
157163
// hit is called here
158164
area.nextPage(NavigationActions.SelectionPolicy.CLEAR);
159165
});
@@ -177,17 +183,20 @@ public void setup() {
177183
});
178184
}
179185

180-
private void runTest() {
186+
private void runTest() throws InterruptedException, ExecutionException {
181187
int start = area.getAbsolutePosition(3, 8);
182-
Bounds b = area.getCharacterBoundsOnScreen(start, start + 1).get();
188+
Bounds b = asyncFx(
189+
() -> area.getCharacterBoundsOnScreen(start, start + 1).get())
190+
.get();
183191
moveTo(b).clickOn(PRIMARY);
184192
assertEquals(start, area.getCaretPosition());
185193
}
186194

187195
public class And_Area_Is_Padded {
188196

189197
@Test
190-
public void clicking_character_should_move_caret_to_that_position() {
198+
public void clicking_character_should_move_caret_to_that_position()
199+
throws InterruptedException, ExecutionException {
191200
interact(() -> area.setPadding(new Insets(PADDING_AMOUNT)));
192201

193202
runTest();
@@ -197,7 +206,8 @@ public void clicking_character_should_move_caret_to_that_position() {
197206
public class And_Area_Is_Not_Padded {
198207

199208
@Test
200-
public void clicking_character_should_move_caret_to_that_position() {
209+
public void clicking_character_should_move_caret_to_that_position()
210+
throws InterruptedException, ExecutionException {
201211
runTest();
202212
}
203213

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package org.fxmisc.richtext.api;
2+
3+
import com.nitorcreations.junit.runners.NestedRunner;
4+
import javafx.stage.Stage;
5+
import org.fxmisc.richtext.CaretNode;
6+
import org.fxmisc.richtext.InlineCssTextArea;
7+
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
8+
import org.fxmisc.richtext.Selection;
9+
import org.fxmisc.richtext.SelectionImpl;
10+
import org.junit.Test;
11+
import org.junit.runner.RunWith;
12+
13+
import static org.junit.Assert.assertEquals;
14+
import static org.junit.Assert.assertFalse;
15+
import static org.junit.Assert.assertTrue;
16+
import static org.junit.Assert.fail;
17+
18+
@RunWith(NestedRunner.class)
19+
public class MultipleCaretSelectionTests extends InlineCssTextAreaAppTest {
20+
21+
@Override
22+
public void start(Stage stage) throws Exception {
23+
super.start(stage);
24+
25+
area.replaceText("first line\nsecond line\nthird line");
26+
}
27+
28+
@Test
29+
public void adding_caret_works() {
30+
CaretNode caret = new CaretNode("test caret", area, 0);
31+
32+
interact(() -> assertTrue(area.addCaret(caret)));
33+
assertTrue(caret.getCaretBounds().isPresent());
34+
assertEquals(0, caret.getPosition());
35+
}
36+
37+
@Test
38+
public void removing_caret_works() {
39+
CaretNode caret = new CaretNode("test caret", area, 0);
40+
interact(() -> {
41+
assertTrue(area.addCaret(caret));
42+
43+
assertTrue(area.removeCaret(caret));
44+
});
45+
}
46+
47+
@Test
48+
public void adding_selection_works() {
49+
Selection<String, String, String> selection = new SelectionImpl<>("test selection", area);
50+
interact(() -> assertTrue(area.addSelection(selection)));
51+
// no selection made yet
52+
assertFalse(selection.getSelectionBounds().isPresent());
53+
54+
// now bounds should be present
55+
interact(selection::selectAll);
56+
assertTrue(selection.getSelectionBounds().isPresent());
57+
}
58+
59+
@Test
60+
public void removing_selection_works() {
61+
Selection<String, String, String> selection = new SelectionImpl<>("test selection", area);
62+
interact(() -> {
63+
assertTrue(area.addSelection(selection));
64+
assertTrue(area.removeSelection(selection));
65+
});
66+
67+
}
68+
69+
@Test
70+
public void attempting_to_remove_original_caret_fails() {
71+
interact(() ->
72+
assertFalse(area.removeCaret(area.getCaretSelectionBind().getUnderlyingCaret()))
73+
);
74+
}
75+
76+
@Test
77+
public void attempting_to_remove_original_selection_fails() {
78+
interact(() ->
79+
assertFalse(area.removeSelection(area.getCaretSelectionBind().getUnderlyingSelection()))
80+
);
81+
}
82+
83+
@Test
84+
public void attempting_to_add_caret_associated_with_different_area_fails() {
85+
InlineCssTextArea area2 = new InlineCssTextArea();
86+
CaretNode caret = new CaretNode("test caret", area2);
87+
interact(() -> {
88+
try {
89+
area.addCaret(caret);
90+
fail();
91+
} catch (IllegalArgumentException e) {
92+
// cannot add a caret associated with a different area
93+
}
94+
});
95+
}
96+
97+
@Test
98+
public void attempting_to_add_selection_associated_with_different_area_fails() {
99+
InlineCssTextArea area2 = new InlineCssTextArea();
100+
Selection<String, String, String> selection = new SelectionImpl<>("test selection", area2);
101+
interact(() -> {
102+
try {
103+
area.addSelection(selection);
104+
fail();
105+
} catch (IllegalArgumentException e) {
106+
// cannot add a selection associated with a different area
107+
}
108+
});
109+
}
110+
111+
@Test
112+
public void modifying_caret_before_adding_to_area_does_not_throw_exception() {
113+
CaretNode caret = new CaretNode("test caret", area);
114+
interact(() -> {
115+
caret.moveToAreaEnd();
116+
area.addCaret(caret);
117+
118+
caret.moveToParEnd();
119+
area.removeCaret(caret);
120+
121+
caret.moveToParStart();
122+
area.addCaret(caret);
123+
area.removeCaret(caret);
124+
});
125+
}
126+
127+
@Test
128+
public void modifying_selection_before_adding_to_area_does_not_throw_exception() {
129+
Selection<String, String, String> selection = new SelectionImpl<>("test selection", area);
130+
interact(() -> {
131+
selection.selectAll();
132+
area.addSelection(selection);
133+
134+
selection.selectRange(0, 4);
135+
area.removeSelection(selection);
136+
137+
selection.deselect();
138+
area.addSelection(selection);
139+
area.removeSelection(selection);
140+
});
141+
}
142+
143+
}

richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/CaretTests.java renamed to richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/SingleCaretTests.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import static org.junit.Assert.assertFalse;
1010
import static org.junit.Assert.assertTrue;
1111

12-
public class CaretTests extends InlineCssTextAreaAppTest {
12+
public class SingleCaretTests extends InlineCssTextAreaAppTest {
1313

1414
private static final String MANY_PARS_OF_TEXT;
1515

@@ -40,8 +40,10 @@ public void caret_bounds_are_present_after_moving_caret_and_following_it() {
4040
assertTrue(area.getCaretBounds().isPresent());
4141

4242
// move caret outside of viewport
43-
area.moveTo(area.getLength());
44-
area.requestFollowCaret();
43+
interact(() -> {
44+
area.moveTo(area.getLength());
45+
area.requestFollowCaret();
46+
});
4547

4648
// needed for test to pass
4749
WaitForAsyncUtils.waitForFxEvents();
@@ -55,7 +57,7 @@ public void caret_bounds_are_absent_after_moving_caret_without_following_it() {
5557
assertTrue(area.getCaretBounds().isPresent());
5658

5759
// move caret outside of viewport
58-
area.moveTo(area.getLength());
60+
interact(() -> area.moveTo(area.getLength()));
5961

6062
// caret should not be visible
6163
assertFalse(area.getCaretBounds().isPresent());

richtextfx/src/integrationTest/java/org/fxmisc/richtext/keyboard/CutCopyPasteTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ public class When_User_Makes_Selection_Ending_In_Newline_Character {
303303

304304
@Before
305305
public void setup() {
306-
area.selectRange(2, 4);
306+
interact(() -> area.selectRange(2, 4));
307307
}
308308

309309
@Test

0 commit comments

Comments
 (0)