Skip to content

Commit dd1588c

Browse files
committed
Issue #594: Properly calculate the background and underline shapes for non-consecutive ranges
1 parent ff74b4b commit dd1588c

9 files changed

Lines changed: 260 additions & 9 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.fxmisc.richtext;
2+
3+
import static org.junit.Assert.assertNotNull;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
import org.fxmisc.flowless.Cell;
9+
import org.fxmisc.flowless.VirtualFlow;
10+
import org.fxmisc.richtext.UnderlinePath;
11+
12+
import javafx.scene.Node;
13+
import javafx.scene.layout.Region;
14+
import javafx.scene.shape.Path;
15+
import javafx.scene.text.Text;
16+
import javafx.scene.text.TextFlow;
17+
18+
/**
19+
* Contains inspection methods to analyze the scene graph which has been rendered by RichTextFX.
20+
* TestFX tests should subclass this if it needs to run tests on a simple area and needs to inspect
21+
* whether the scene graph has been properly created.
22+
*/
23+
public abstract class SceneGraphTests extends InlineCssTextAreaAppTest {
24+
25+
/**
26+
* @param index The index of the desired paragraph box
27+
* @return The paragraph box for the paragraph at the specified index
28+
*/
29+
protected Region getParagraphBox(int index) {
30+
@SuppressWarnings("unchecked")
31+
VirtualFlow<String, Cell<String, Node>> flow = (VirtualFlow<String, Cell<String, Node>>) area.getChildrenUnmodifiable().get(index);
32+
Cell<String, Node> gsa = flow.getCell(0);
33+
34+
// get the ParagraphBox (protected subclass of Region)
35+
return (Region) gsa.getNode();
36+
}
37+
38+
39+
/**
40+
* @param index The index of the desired paragraph box
41+
* @return The ParagraphText (protected subclass of TextFlow) for the paragraph at the specified index
42+
*/
43+
protected TextFlow getParagraphText(int index) {
44+
// get the ParagraphBox (protected subclass of Region)
45+
Region paragraphBox = getParagraphBox(index);
46+
47+
// get the ParagraphText (protected subclass of TextFlow)
48+
TextFlow tf = (TextFlow) paragraphBox.getChildrenUnmodifiable().stream().filter(n -> n instanceof TextFlow)
49+
.findFirst().orElse(null);
50+
assertNotNull("No TextFlow node found in rich text area", tf);
51+
52+
return tf;
53+
}
54+
55+
56+
/**
57+
* @param index The index of the desired paragraph box
58+
* @return A list of text nodes which render the text in the ParagraphBox
59+
* specified by the given index.
60+
*/
61+
protected List<Text> getTextNodes(int index) {
62+
TextFlow tf = getParagraphText(index);
63+
64+
List<Text> result = new ArrayList<>();
65+
tf.getChildrenUnmodifiable().filtered(n -> n instanceof Text).forEach(n -> result.add((Text) n));
66+
return result;
67+
}
68+
69+
70+
/**
71+
* @param index The index of the desired paragraph box
72+
* @return A list of nodes which render the underlines for the text in the ParagraphBox
73+
* specified by the given index.
74+
*/
75+
protected List<Path> getUnderlinePaths(int index) {
76+
TextFlow tf = getParagraphText(index);
77+
78+
List<Path> result = new ArrayList<>();
79+
tf.getChildrenUnmodifiable().filtered(n -> n instanceof UnderlinePath).forEach(n -> result.add((Path) n));
80+
return result;
81+
}
82+
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import javafx.geometry.Bounds;
44
import javafx.stage.Stage;
55
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
6-
import org.junit.Ignore;
76
import org.junit.Test;
87

98
import static javafx.scene.input.KeyCode.PAGE_DOWN;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.fxmisc.richtext.style;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.util.List;
6+
7+
import org.fxmisc.richtext.SceneGraphTests;
8+
import org.junit.Test;
9+
10+
import javafx.scene.shape.Path;
11+
import javafx.scene.text.Text;
12+
13+
14+
public class StylingTests extends SceneGraphTests {
15+
16+
private final static String HELLO = "Hello ";
17+
private final static String WORLD = "World";
18+
private final static String AND_ALSO_THE = " and also the ";
19+
private final static String SUN = "Sun";
20+
private final static String AND_MOON = " and Moon";
21+
22+
@Test
23+
public void simpleStyling() {
24+
// setup
25+
interact(() -> {
26+
area.replaceText(HELLO + WORLD + AND_MOON);
27+
});
28+
29+
// expected: one text node which contains the complete text
30+
List<Text> textNodes = getTextNodes(0);
31+
assertEquals(1, textNodes.size());
32+
33+
interact(() -> {
34+
area.setStyle(HELLO.length(), HELLO.length() + WORLD.length(), "-fx-font-weight: bold;");
35+
});
36+
37+
// expected: three text nodes
38+
textNodes = getTextNodes(0);
39+
assertEquals(3, textNodes.size());
40+
41+
Text first = textNodes.get(0);
42+
assertEquals("Hello ", first.getText());
43+
assertEquals("Regular", first.getFont().getStyle());
44+
45+
Text second = textNodes.get(1);
46+
assertEquals("World", second.getText());
47+
assertEquals("Bold", second.getFont().getStyle());
48+
49+
Text third = textNodes.get(2);
50+
assertEquals(" and Moon", third.getText());
51+
assertEquals("Regular", third.getFont().getStyle());
52+
}
53+
54+
55+
@Test
56+
public void underlineStyling() {
57+
58+
final String underlineStyle = "-rtfx-underline-color: red; -rtfx-underline-dash-array: 2 2; -rtfx-underline-width: 1; -rtfx-underline-cap: butt;";
59+
60+
// setup
61+
interact(() -> {
62+
area.replaceText(HELLO + WORLD + AND_ALSO_THE + SUN + AND_MOON);
63+
});
64+
65+
// expected: one text node which contains the complete text
66+
List<Text> textNodes = getTextNodes(0);
67+
assertEquals(1, textNodes.size());
68+
assertEquals(HELLO + WORLD + AND_ALSO_THE + SUN + AND_MOON,
69+
textNodes.get(0).getText());
70+
71+
interact(() -> {
72+
final int start1 = HELLO.length();
73+
final int end1 = start1 + WORLD.length();
74+
area.setStyle(start1, end1, underlineStyle);
75+
76+
final int start2 = end1 + AND_ALSO_THE.length();
77+
final int end2 = start2 + SUN.length();
78+
area.setStyle(start2, end2, underlineStyle);
79+
});
80+
81+
// expected: five text nodes
82+
textNodes = getTextNodes(0);
83+
assertEquals(5, textNodes.size());
84+
85+
Text first = textNodes.get(0);
86+
assertEquals(HELLO, first.getText());
87+
Text second = textNodes.get(1);
88+
assertEquals(WORLD, second.getText());
89+
Text third = textNodes.get(2);
90+
assertEquals(AND_ALSO_THE, third.getText());
91+
Text fourth = textNodes.get(3);
92+
assertEquals(SUN, fourth.getText());
93+
Text fifth = textNodes.get(4);
94+
assertEquals(AND_MOON, fifth.getText());
95+
96+
// determine the underline paths - need to be two of them!
97+
List<Path> underlineNodes = getUnderlinePaths(0);
98+
assertEquals(2, underlineNodes.size());
99+
}
100+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.fxmisc.richtext;
2+
3+
import javafx.scene.shape.Path;
4+
5+
/**
6+
* A path which describes a background shape in the Scene graph.
7+
*
8+
*/
9+
public class BackgroundPath extends Path {
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.fxmisc.richtext;
2+
3+
import javafx.scene.shape.Path;
4+
5+
/**
6+
* A path which describes a border in the Scene graph.
7+
*
8+
*/
9+
public class BorderPath extends Path {
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.fxmisc.richtext;
2+
3+
import javafx.scene.shape.Path;
4+
5+
/**
6+
* A path which describes a caret shape in the Scene graph.
7+
*
8+
*/
9+
public class CaretPath extends Path {
10+
}

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

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
5656

5757
private final Paragraph<PS, SEG, S> paragraph;
5858

59-
private final Path caretShape = new Path();
60-
private final Path selectionShape = new Path();
59+
private final Path caretShape = new CaretPath();
60+
private final Path selectionShape = new SelectionPath();
6161

6262
private final CustomCssShapeHelper<Paint> backgroundShapeHelper;
6363
private final CustomCssShapeHelper<BorderAttributes> borderShapeHelper;
@@ -115,18 +115,33 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
115115
par.getStyledSegments().stream().map(nodeFactory).forEach(getChildren()::add);
116116

117117
// set up custom css shape helpers
118-
Supplier<Path> createShape = () -> {
119-
Path shape = new Path();
118+
Supplier<Path> createBackgroundShape = () -> {
119+
Path shape = new BackgroundPath();
120120
shape.setManaged(false);
121121
shape.layoutXProperty().bind(leftInset);
122122
shape.layoutYProperty().bind(topInset);
123123
return shape;
124124
};
125+
Supplier<Path> createBorderShape = () -> {
126+
Path shape = new BorderPath();
127+
shape.setManaged(false);
128+
shape.layoutXProperty().bind(leftInset);
129+
shape.layoutYProperty().bind(topInset);
130+
return shape;
131+
};
132+
Supplier<Path> createUnderlineShape = () -> {
133+
Path shape = new UnderlinePath();
134+
shape.setManaged(false);
135+
shape.layoutXProperty().bind(leftInset);
136+
shape.layoutYProperty().bind(topInset);
137+
return shape;
138+
};
139+
125140
Consumer<Collection<Path>> clearUnusedShapes = paths -> getChildren().removeAll(paths);
126141
Consumer<Path> addToBackground = path -> getChildren().add(0, path);
127142
Consumer<Path> addToForeground = path -> getChildren().add(path);
128143
backgroundShapeHelper = new CustomCssShapeHelper<>(
129-
createShape,
144+
createBackgroundShape,
130145
(backgroundShape, tuple) -> {
131146
backgroundShape.setStrokeWidth(0);
132147
backgroundShape.setFill(tuple._1);
@@ -136,7 +151,7 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
136151
clearUnusedShapes
137152
);
138153
borderShapeHelper = new CustomCssShapeHelper<>(
139-
createShape,
154+
createBorderShape,
140155
(borderShape, tuple) -> {
141156
BorderAttributes attributes = tuple._1;
142157
borderShape.setStrokeWidth(attributes.width);
@@ -153,7 +168,7 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
153168
clearUnusedShapes
154169
);
155170
underlineShapeHelper = new CustomCssShapeHelper<>(
156-
createShape,
171+
createUnderlineShape,
157172
(underlineShape, tuple) -> {
158173
UnderlineAttributes attributes = tuple._1;
159174
underlineShape.setStroke(attributes.color);
@@ -389,7 +404,12 @@ private void updateSharedShapeRange(T value, int start, int end) {
389404
int lastIndex = ranges.size() - 1;
390405
Tuple2<T, IndexRange> lastShapeValueRange = ranges.get(lastIndex);
391406
T lastShapeValue = lastShapeValueRange._1;
392-
if (lastShapeValue.equals(value)) {
407+
408+
// calculate smallest possible position which is consecutive to the given start position
409+
final int prevEndNext = lastShapeValueRange.get2().getEnd() + 1;
410+
if (start <= prevEndNext && // Consecutive?
411+
lastShapeValue.equals(value)) { // Same style?
412+
393413
IndexRange lastRange = lastShapeValueRange._2;
394414
IndexRange extendedRange = new IndexRange(lastRange.getStart(), end);
395415
ranges.set(lastIndex, Tuples.t(lastShapeValue, extendedRange));
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.fxmisc.richtext;
2+
3+
import javafx.scene.shape.Path;
4+
5+
/**
6+
* A path which describes a selection shape in the Scene graph.
7+
*
8+
*/
9+
public class SelectionPath extends Path {
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.fxmisc.richtext;
2+
3+
import javafx.scene.shape.Path;
4+
5+
/**
6+
* A path which describes an underline in the Scene graph.
7+
*
8+
*/
9+
public class UnderlinePath extends Path {
10+
}

0 commit comments

Comments
 (0)