Skip to content

Commit 7c32514

Browse files
committed
Merge pull request #317 from afester/dottedUnderline
Support underlined text with some customization of the underline.
2 parents 2d34cef + 45b1241 commit 7c32514

7 files changed

Lines changed: 429 additions & 11 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.fxmisc.richtext.demo;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.io.InputStreamReader;
7+
import java.text.BreakIterator;
8+
import java.util.Collection;
9+
import java.util.Collections;
10+
import java.util.HashSet;
11+
import java.util.Set;
12+
13+
import org.fxmisc.flowless.VirtualizedScrollPane;
14+
import org.fxmisc.richtext.StyleClassedTextArea;
15+
import org.fxmisc.richtext.StyleSpans;
16+
import org.fxmisc.richtext.StyleSpansBuilder;
17+
18+
import javafx.application.Application;
19+
import javafx.scene.Scene;
20+
import javafx.scene.layout.StackPane;
21+
import javafx.stage.Stage;
22+
23+
public class SpellChecking extends Application {
24+
25+
private static final Set<String> dictionary = new HashSet<String>();
26+
27+
public static void main(String[] args) {
28+
launch(args);
29+
}
30+
31+
@Override
32+
public void start(Stage primaryStage) {
33+
StyleClassedTextArea textArea = new StyleClassedTextArea();
34+
textArea.setWrapText(true);
35+
36+
textArea.richChanges()
37+
.filter(ch -> !ch.getInserted().equals(ch.getRemoved())) // XXX
38+
.subscribe(change -> {
39+
textArea.setStyleSpans(0, computeHighlighting(textArea.getText()));
40+
});
41+
42+
// load the dictionary
43+
try (InputStream input = getClass().getResourceAsStream("spellchecking.dict");
44+
BufferedReader br = new BufferedReader(new InputStreamReader(input))) {
45+
String line;
46+
while ((line = br.readLine()) != null) {
47+
dictionary.add(line);
48+
}
49+
} catch (IOException e) {
50+
e.printStackTrace();
51+
}
52+
53+
// load the sample document
54+
InputStream input2 = getClass().getResourceAsStream("spellchecking.txt");
55+
try(java.util.Scanner s = new java.util.Scanner(input2)) {
56+
String document = s.useDelimiter("\\A").hasNext() ? s.next() : "";
57+
textArea.replaceText(0, 0, document);
58+
}
59+
60+
Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(textArea)), 600, 400);
61+
scene.getStylesheets().add(getClass().getResource("spellchecking.css").toExternalForm());
62+
primaryStage.setScene(scene);
63+
primaryStage.setTitle("Spell Checking Demo");
64+
primaryStage.show();
65+
}
66+
67+
68+
private static StyleSpans<Collection<String>> computeHighlighting(String text) {
69+
70+
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
71+
72+
BreakIterator wb = BreakIterator.getWordInstance();
73+
wb.setText(text);
74+
75+
int lastIndex = wb.first();
76+
int lastKwEnd = 0;
77+
while(lastIndex != BreakIterator.DONE) {
78+
int firstIndex = lastIndex;
79+
lastIndex = wb.next();
80+
81+
if (lastIndex != BreakIterator.DONE
82+
&& Character.isLetterOrDigit(text.charAt(firstIndex))) {
83+
String word = text.substring(firstIndex, lastIndex).toLowerCase();
84+
if (!dictionary.contains(word)) {
85+
spansBuilder.add(Collections.emptyList(), firstIndex - lastKwEnd);
86+
spansBuilder.add(Collections.singleton("underlined"), lastIndex - firstIndex);
87+
lastKwEnd = lastIndex;
88+
}
89+
System.err.println();
90+
}
91+
}
92+
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
93+
94+
return spansBuilder.create();
95+
}
96+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.underlined {
2+
-fx-underline-color: red;
3+
-fx-underline-dash-array: 2 2;
4+
-fx-underline-width: 1;
5+
-fx-underline-cap: butt;
6+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
a
2+
applied
3+
basic
4+
brown
5+
but
6+
could
7+
document
8+
dog
9+
fox
10+
here
11+
if
12+
is
13+
its
14+
jumps
15+
lazy
16+
no
17+
over
18+
quick
19+
rendering
20+
sample
21+
see
22+
styling
23+
the
24+
there
25+
this
26+
were
27+
you
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The quik brown fox jumps over the lazy dog.
2+
Ths is a sample dokument.
3+
There is no styling aplied, but if there were, you could see its basic rndering here.

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

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.fxmisc.richtext;
22

33
import java.util.ArrayList;
4+
import java.util.Arrays;
45
import java.util.List;
56
import java.util.Optional;
67
import java.util.function.BiConsumer;
@@ -17,6 +18,7 @@
1718
import javafx.scene.paint.Paint;
1819
import javafx.scene.shape.Path;
1920
import javafx.scene.shape.PathElement;
21+
import javafx.scene.shape.StrokeLineCap;
2022

2123
import org.reactfx.value.Val;
2224
import org.reactfx.value.Var;
@@ -45,6 +47,7 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
4547
private final Path caretShape = new Path();
4648
private final Path selectionShape = new Path();
4749
private final List<Path> backgroundShapes = new ArrayList<>();
50+
private final List<Path> underlineShapes = new ArrayList<>();
4851

4952
// proxy for caretShape.visibleProperty() that implements unbind() correctly.
5053
// 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
108111
getChildren().add(t);
109112

110113
// add corresponding background node (empty)
111-
112114
Path backgroundShape = new Path();
113115
backgroundShape.setManaged(false);
114116
backgroundShape.setStrokeWidth(0);
115117
backgroundShape.layoutXProperty().bind(leftInset);
116118
backgroundShape.layoutYProperty().bind(topInset);
117119
backgroundShapes.add(backgroundShape);
118120
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);
119130
}
120131
}
121132

@@ -181,23 +192,101 @@ private void updateBackgroundShapes() {
181192
FilteredList<Node> nodeList = getChildren().filtered(node -> node instanceof TextExt);
182193
for (Node node : nodeList) {
183194
TextExt text = (TextExt) node;
184-
Path backgroundShape = backgroundShapes.get(index++);
185195
int end = start + text.getText().length();
186196

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+
191227

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();
195259
}
260+
}
196261

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);
198286
}
199287
}
200288

289+
201290
@Override
202291
protected void layoutChildren() {
203292
super.layoutChildren();

0 commit comments

Comments
 (0)