diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java index 787f34e7..e816d4b1 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java @@ -216,7 +216,7 @@ public ObjectProperty highlightTextFillProperty() { underlineShape.getStrokeDashArray().setAll(attributes.dashArray); } PathElement[] shape = getUnderlineShape(tuple._2.getStart(), tuple._2.getEnd(), - attributes.offset, attributes.waveRadius); + attributes.offset, attributes.waveRadius, attributes.doubleGap); underlineShape.getElements().setAll(shape); }, addToForeground, @@ -604,16 +604,23 @@ private static class UnderlineAttributes extends LineAttributesBase { final StrokeLineCap cap; final double offset; final double waveRadius; + final double doubleGap; UnderlineAttributes(TextExt text) { super(text.getUnderlineColor(), text.getUnderlineWidth(), text.underlineDashArrayProperty()); cap = text.getUnderlineCap(); + Number waveNumber = text.getUnderlineWaveRadius(); waveRadius = waveNumber == null ? 0 : waveNumber.doubleValue(); + Number offsetNumber = text.getUnderlineOffset(); offset = offsetNumber == null ? waveRadius * 0.5 : offsetNumber.doubleValue(); // The larger the radius the bigger the offset needs to be, so // a reasonable default is provided if no offset is specified. + + Number doubleGapNumber = text.getUnderlineDoubleGap(); + if (doubleGapNumber == null) doubleGap = 0; + else doubleGap = doubleGapNumber.doubleValue() + width; } /** @@ -621,7 +628,8 @@ private static class UnderlineAttributes extends LineAttributesBase { */ public boolean equalsFaster(UnderlineAttributes attr) { return super.equalsFaster(attr) && Objects.equals(cap, attr.cap) - && offset == attr.offset && waveRadius == attr.waveRadius; + && offset == attr.offset && waveRadius == attr.waveRadius + && doubleGap == attr.doubleGap; } @Override diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/TextExt.java b/richtextfx/src/main/java/org/fxmisc/richtext/TextExt.java index 611a192e..735e7df7 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/TextExt.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/TextExt.java @@ -46,6 +46,7 @@ public class TextExt extends Text { styleables.add(StyleableProperties.UNDERLINE_OFFSET); styleables.add(StyleableProperties.UNDERLINE_WAVE_RADIUS); styleables.add(StyleableProperties.UNDERLINE_DASH_ARRAY); + styleables.add(StyleableProperties.UNDERLINE_DOUBLE_GAP); styleables.add(StyleableProperties.UNDERLINE_CAP); CSS_META_DATA_LIST = Collections.unmodifiableList(styleables); @@ -91,6 +92,10 @@ public class TextExt extends Text { null, "underlineDashArray", this, StyleableProperties.UNDERLINE_DASH_ARRAY ); + private final StyleableObjectProperty underlineDoubleGap = new CustomStyleableProperty<>( + null, "underlineDoubleGap", this, StyleableProperties.UNDERLINE_DOUBLE_GAP + ); + private final StyleableObjectProperty underlineCap = new CustomStyleableProperty<>( null, "underlineCap", this, StyleableProperties.UNDERLINE_CAP ); @@ -266,6 +271,22 @@ public ObjectProperty borderStrokeColorProperty() { */ public ObjectProperty underlineWaveRadiusProperty() { return underlineWaveRadius; } + public Number getUnderlineDoubleGap() { return underlineDoubleGap.get(); } + public void setUnderlineDoubleGap(Number radius) { underlineDoubleGap.set(radius); } + + /** + * The size of the gap between two parallel underline lines or wave forms. If null or zero, the + * underline will be a single line or wave form. + * + * Can be styled from CSS using the "-rtfx-underline-double-gap" property. + * + *

Note that the underline properties specified here are orthogonal to the {@link #underlineProperty()} inherited + * from {@link Text}. The underline properties defined here in {@link TextExt} will cause an underline to be + * drawn if {@link #underlineWidthProperty()} is non-null and greater than zero, regardless of + * the value of {@link #underlineProperty()}.

+ */ + public ObjectProperty underlineDoubleGapProperty() { return underlineDoubleGap; } + // Dash array for the text underline public Number[] getUnderlineDashArray() { return underlineDashArray.get(); } public void setUnderlineDashArray(Number[] dashArray) { underlineDashArray.set(dashArray); } @@ -315,7 +336,7 @@ private static class StyleableProperties { ); private static final CssMetaData BORDER_TYPE = new CustomCssMetaData<>( - "-rtfx-border-stroke-type", (StyleConverter) StyleConverter.getEnumConverter(StrokeType.class), + "-rtfx-border-stroke-type", StyleConverter.getEnumConverter(StrokeType.class), StrokeType.INSIDE, n -> n.borderStrokeType ); @@ -349,8 +370,13 @@ private static class StyleableProperties { new Double[0], n -> n.underlineDashArray ); + private static final CssMetaData UNDERLINE_DOUBLE_GAP = new CustomCssMetaData<>( + "-rtfx-underline-double-gap", StyleConverter.getSizeConverter(), + 0, n -> n.underlineDoubleGap + ); + private static final CssMetaData UNDERLINE_CAP = new CustomCssMetaData<>( - "-rtfx-underline-cap", (StyleConverter) StyleConverter.getEnumConverter(StrokeLineCap.class), + "-rtfx-underline-cap", StyleConverter.getEnumConverter(StrokeLineCap.class), StrokeLineCap.SQUARE, n -> n.underlineCap ); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java b/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java index 522d9c77..10a972ec 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java @@ -69,7 +69,7 @@ PathElement[] getUnderlineShape(IndexRange range) { } PathElement[] getUnderlineShape(int from, int to) { - return getUnderlineShape(from, to, 0, 0); + return getUnderlineShape(from, to, 0, 0, 0); } /** @@ -80,7 +80,7 @@ PathElement[] getUnderlineShape(int from, int to) { * @return An array with the PathElement objects which define an * underline from the first to the last character. */ - PathElement[] getUnderlineShape(int from, int to, double offset, double waveRadius) { + PathElement[] getUnderlineShape(int from, int to, double offset, double waveRadius, double doubleGap) { // get a Path for the text underline List result = new ArrayList<>(); @@ -88,6 +88,9 @@ PathElement[] getUnderlineShape(int from, int to, double offset, double waveRadi // The shape is a closed Path for one or more rectangles AROUND the selected text. // shape: [MoveTo origin, LineTo top R, LineTo bottom R, LineTo bottom L, LineTo origin, *] + boolean doubleLine = (doubleGap > 0.0); + List result2 = new ArrayList<>(); + // Extract the bottom left and right coordinates for each rectangle to get the underline path. for ( int ele = 2; ele < shape.length; ele += 5 ) { @@ -100,12 +103,20 @@ PathElement[] getUnderlineShape(int from, int to, double offset, double waveRadi if (waveRadius <= 0) { result.add(new MoveTo( leftx, y )); result.add(new LineTo( snapSizeX( br.getX() ), y )); + if (doubleLine) { + y += doubleGap; + result2.add(new MoveTo( leftx, y )); + result2.add(new LineTo( snapSizeX( br.getX() ), y )); + } } else { // For larger wave radii increase the X radius to stretch out the wave. double radiusX = waveRadius > 1 ? waveRadius * 1.25 : waveRadius; double rightx = br.getX(); result.add(new MoveTo( leftx, y )); + if (doubleLine) { + result2.add(new MoveTo( leftx, y+doubleGap )); + } boolean sweep = true; while ( leftx < rightx ) { leftx += waveRadius * 2; @@ -127,10 +138,15 @@ PathElement[] getUnderlineShape(int from, int to, double offset, double waveRadi leftx = rightx; } result.add(new ArcTo( radiusX, waveRadius, 0.0, leftx, y, false, sweep )); + if (doubleLine) { + result2.add(new ArcTo( radiusX, waveRadius, 0.0, leftx, y+doubleGap, false, sweep )); + } sweep = !sweep; } } } + + if (doubleLine) result.addAll( result2 ); return result.toArray(new PathElement[0]); }