|
1 | 1 | package org.fxmisc.richtext; |
2 | 2 |
|
3 | 3 | import java.util.ArrayList; |
| 4 | +import java.util.Arrays; |
4 | 5 | import java.util.List; |
5 | 6 | import java.util.Optional; |
6 | 7 | import java.util.function.BiConsumer; |
|
17 | 18 | import javafx.scene.paint.Paint; |
18 | 19 | import javafx.scene.shape.Path; |
19 | 20 | import javafx.scene.shape.PathElement; |
| 21 | +import javafx.scene.shape.StrokeLineCap; |
20 | 22 |
|
21 | 23 | import org.reactfx.value.Val; |
22 | 24 | import org.reactfx.value.Var; |
@@ -45,6 +47,7 @@ public ObjectProperty<Paint> highlightTextFillProperty() { |
45 | 47 | private final Path caretShape = new Path(); |
46 | 48 | private final Path selectionShape = new Path(); |
47 | 49 | private final List<Path> backgroundShapes = new ArrayList<>(); |
| 50 | + private final List<Path> underlineShapes = new ArrayList<>(); |
48 | 51 |
|
49 | 52 | // proxy for caretShape.visibleProperty() that implements unbind() correctly. |
50 | 53 | // This is necessary due to a bug in BooleanPropertyBase#unbind(). |
@@ -108,14 +111,22 @@ public ParagraphText(Paragraph<PS, S> par, BiConsumer<? super TextExt, S> applyS |
108 | 111 | getChildren().add(t); |
109 | 112 |
|
110 | 113 | // add corresponding background node (empty) |
111 | | - |
112 | 114 | Path backgroundShape = new Path(); |
113 | 115 | backgroundShape.setManaged(false); |
114 | 116 | backgroundShape.setStrokeWidth(0); |
115 | 117 | backgroundShape.layoutXProperty().bind(leftInset); |
116 | 118 | backgroundShape.layoutYProperty().bind(topInset); |
117 | 119 | backgroundShapes.add(backgroundShape); |
118 | 120 | getChildren().add(0, backgroundShape); |
| 121 | + |
| 122 | + // add corresponding underline node (empty) |
| 123 | + Path underlineShape = new Path(); |
| 124 | + underlineShape.setManaged(false); |
| 125 | + underlineShape.setStrokeWidth(0); |
| 126 | + underlineShape.layoutXProperty().bind(leftInset); |
| 127 | + underlineShape.layoutYProperty().bind(topInset); |
| 128 | + underlineShapes.add(underlineShape); |
| 129 | + getChildren().add(0, underlineShape); |
119 | 130 | } |
120 | 131 | } |
121 | 132 |
|
@@ -181,23 +192,101 @@ private void updateBackgroundShapes() { |
181 | 192 | FilteredList<Node> nodeList = getChildren().filtered(node -> node instanceof TextExt); |
182 | 193 | for (Node node : nodeList) { |
183 | 194 | TextExt text = (TextExt) node; |
184 | | - Path backgroundShape = backgroundShapes.get(index++); |
185 | 195 | int end = start + text.getText().length(); |
186 | 196 |
|
187 | | - // Set fill |
188 | | - Paint paint = text.backgroundFillProperty().get(); |
189 | | - if (paint != null) { |
190 | | - backgroundShape.setFill(paint); |
| 197 | + updateBackground(text, start, end, index); |
| 198 | + updateUnderline(text, start, end, index); |
| 199 | + |
| 200 | + start = end; |
| 201 | + index++; |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + |
| 206 | + /** |
| 207 | + * Updates the background shape for a text segment. |
| 208 | + * |
| 209 | + * @param text The text node which specified the style attributes |
| 210 | + * @param start The index of the first character |
| 211 | + * @param end The index of the last character |
| 212 | + * @param index The index of the background shape |
| 213 | + */ |
| 214 | + private void updateBackground(TextExt text, int start, int end, int index) { |
| 215 | + // Set fill |
| 216 | + Paint paint = text.backgroundFillProperty().get(); |
| 217 | + if (paint != null) { |
| 218 | + Path backgroundShape = backgroundShapes.get(index); |
| 219 | + backgroundShape.setFill(paint); |
| 220 | + |
| 221 | + // Set path elements |
| 222 | + PathElement[] shape = getRangeShape(start, end); |
| 223 | + backgroundShape.getElements().setAll(shape); |
| 224 | + } |
| 225 | + } |
| 226 | + |
191 | 227 |
|
192 | | - // Set path elements |
193 | | - PathElement[] shape = getRangeShape(start, end); |
194 | | - backgroundShape.getElements().setAll(shape); |
| 228 | + /** |
| 229 | + * Updates the shape which renders the text underline. |
| 230 | + * |
| 231 | + * @param text The text node which specified the style attributes |
| 232 | + * @param start The index of the first character |
| 233 | + * @param end The index of the last character |
| 234 | + * @param index The index of the background shape |
| 235 | + */ |
| 236 | + private void updateUnderline(TextExt text, int start, int end, int index) { |
| 237 | + |
| 238 | + // get all CSS properties for the underline |
| 239 | + |
| 240 | + Paint underlineColor = text.underlineColorProperty().get(); |
| 241 | + Number underlineWidth = text.underlineWidthProperty().get(); |
| 242 | + |
| 243 | + // get the dash array - JavaFX CSS parser seems to return either a Number[] array |
| 244 | + // or a single value, depending on whether only one or more than one value has been |
| 245 | + // specified in the CSS |
| 246 | + Double[] underlineDashArray = null; |
| 247 | + Object underlineDashArrayProp = text.underlineDashArrayProperty().get(); |
| 248 | + if (underlineDashArrayProp != null) { |
| 249 | + if (underlineDashArrayProp.getClass().isArray()) { |
| 250 | + Number[] numberArray = (Number[]) underlineDashArrayProp; |
| 251 | + underlineDashArray = new Double[numberArray.length]; |
| 252 | + int idx = 0; |
| 253 | + for (Number d : numberArray) { |
| 254 | + underlineDashArray[idx++] = (Double) d; |
| 255 | + } |
| 256 | + } else { |
| 257 | + underlineDashArray = new Double[1]; |
| 258 | + underlineDashArray[0] = ((Double) underlineDashArrayProp).doubleValue(); |
195 | 259 | } |
| 260 | + } |
196 | 261 |
|
197 | | - start = end; |
| 262 | + StrokeLineCap underlineCap = text.underlineCapProperty().get(); |
| 263 | + |
| 264 | + // apply style and render the underline |
| 265 | + |
| 266 | + Path underlineShape = underlineShapes.get(index); |
| 267 | + if (underlineColor != null) { |
| 268 | + underlineShape.setStroke(underlineColor); |
| 269 | + } |
| 270 | + if (underlineWidth != null) { |
| 271 | + underlineShape.setStrokeWidth(underlineWidth.doubleValue()); |
| 272 | + } |
| 273 | + if (underlineDashArray != null) { |
| 274 | + underlineShape.getStrokeDashArray().addAll(underlineDashArray); |
| 275 | + underlineShape.setStrokeLineCap(StrokeLineCap.BUTT); |
| 276 | + } |
| 277 | + if (underlineCap != null) { |
| 278 | + underlineShape.setStrokeLineCap(underlineCap); |
| 279 | + } |
| 280 | + |
| 281 | + if (underlineColor != null || underlineWidth != null) { |
| 282 | + |
| 283 | + // Set path elements |
| 284 | + PathElement[] shape = getUnderlineShape(start, end); |
| 285 | + underlineShape.getElements().setAll(shape); |
198 | 286 | } |
199 | 287 | } |
200 | 288 |
|
| 289 | + |
201 | 290 | @Override |
202 | 291 | protected void layoutChildren() { |
203 | 292 | super.layoutChildren(); |
|
0 commit comments