From 4cca5e012d4c2c716af5a9f42157183075c238fc Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 2 Aug 2017 01:01:23 -0700 Subject: [PATCH] Only use 1 shape for consecutive TextExts with the same underline --- .../org/fxmisc/richtext/ParagraphText.java | 258 ++++++++++-------- .../java/org/fxmisc/richtext/TextFlowExt.java | 4 + 2 files changed, 144 insertions(+), 118 deletions(-) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java index 8e16e4bdd..fbd53047d 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java @@ -1,13 +1,20 @@ package org.fxmisc.richtext; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.geometry.Bounds; import javafx.geometry.Insets; @@ -49,9 +56,10 @@ public ObjectProperty highlightTextFillProperty() { private final Path caretShape = new Path(); private final Path selectionShape = new Path(); private final List backgroundShapes = new LinkedList<>(); - private final List underlineShapes; + private final List underlineShapes = new LinkedList<>(); private final List> backgroundColorRanges = new LinkedList<>(); + private final List> underlineRanges = new LinkedList<>(); private final Val leftInset; private final Val topInset; @@ -102,17 +110,12 @@ public ObjectProperty highlightTextFillProperty() { // text.impl_selectionFillProperty().set(newFill); // } // }); - underlineShapes = new ArrayList<>(); // populate with text nodes for(SEG segment: par.getSegments()) { // create Segment Node fxNode = nodeFactory.apply(segment); getChildren().add(fxNode); - - // add placeholder to prevent IOOBE; only create shapes when needed - underlineShapes.add(null); - } } @@ -199,164 +202,183 @@ private void updateSelectionShape() { } private void updateBackgroundShapes() { - int index = 0; int start = 0; + // calculate shared values among consecutive nodes FilteredList nodeList = getChildren().filtered(node -> node instanceof TextExt); for (Node node : nodeList) { TextExt text = (TextExt) node; int end = start + text.getText().length(); - calculateBackgroundColorRange(text, start, end); + Paint backgroundColor = text.getBackgroundColor(); + if (backgroundColor != null) { + updateSharedShapeRange(backgroundColorRanges, backgroundColor, start, end); + } - updateUnderline(text, start, end, index); + UnderlineAttributes attributes = new UnderlineAttributes(text); + if (!attributes.isNullValue()) { + updateSharedShapeRange(underlineRanges, attributes, start, end); + } start = end; - index++; } - updateBackgroundColorShapes(); + // now only use one shape per shared value + updateSharedShapes(backgroundColorRanges, backgroundShapes, (children, shape) -> children.add(0, shape), + (colorShape, tuple) -> { + colorShape.setStrokeWidth(0); + colorShape.setFill(tuple._1); + colorShape.getElements().setAll(getRangeShape(tuple._2)); + }); + updateSharedShapes(underlineRanges, underlineShapes, (children, shape) -> children.add(shape), + (underlineShape, tuple) -> { + UnderlineAttributes attributes = tuple._1; + underlineShape.setStroke(attributes.color); + underlineShape.setStrokeWidth(attributes.width); + underlineShape.setStrokeLineCap(attributes.cap); + if (attributes.dashArray != null) { + underlineShape.getStrokeDashArray().setAll(attributes.dashArray); + } + underlineShape.getElements().setAll(getUnderlineShape(tuple._2)); + }); } /** - * Calculates the range of a background color that is shared between multiple consecutive {@link TextExt} nodes + * Calculates the range of a value (background color, underline, etc.) that is shared between multiple + * consecutive {@link TextExt} nodes */ - private void calculateBackgroundColorRange(TextExt text, int start, int end) { - Paint backgroundColor = text.getBackgroundColor(); - if (backgroundColor != null) { - Runnable addNewColor = () -> backgroundColorRanges.add(Tuples.t(backgroundColor, new IndexRange(start, end))); + private void updateSharedShapeRange(List> rangeList, T value, int start, int end) { + updateSharedShapeRange0( + rangeList, + () -> Tuples.t(value, new IndexRange(start, end)), + lastRange -> { + T lastShapeValue = lastRange._1; + return lastShapeValue.equals(value); + }, + lastRange -> lastRange.map((val, range) -> Tuples.t(val, new IndexRange(range.getStart(), end))) + ); + } - if (backgroundColorRanges.isEmpty()) { - addNewColor.run(); + private void updateSharedShapeRange0(List rangeList, Supplier newValueRange, + Predicate sharesShapeValue, UnaryOperator mapper) { + if (rangeList.isEmpty()) { + rangeList.add(newValueRange.get()); + } else { + int lastIndex = rangeList.size() - 1; + T lastShapeValueRange = rangeList.get(lastIndex); + if (sharesShapeValue.test(lastShapeValueRange)) { + rangeList.set(lastIndex, mapper.apply(lastShapeValueRange)); } else { - int lastIndex = backgroundColorRanges.size() - 1; - Tuple2 lastColorRange = backgroundColorRanges.get(lastIndex); - Paint lastColor = lastColorRange._1; - if (lastColor.equals(backgroundColor)) { - IndexRange colorRange = lastColorRange._2; - backgroundColorRanges.set(lastIndex, Tuples.t(backgroundColor, new IndexRange(colorRange.getStart(), end))); - } else { - addNewColor.run(); - } + rangeList.add(newValueRange.get()); } } } - private void updateBackgroundColorShapes() { + /** + * Updates the shapes calculated in {@link #updateSharedShapeRange(List, Object, int, int)} and configures them + * via {@code configureShape}. + */ + private void updateSharedShapes(List rangeList, List shapeList, + BiConsumer, Path> addToChildren, + BiConsumer configureShape) { // remove or add shapes, depending on what's needed - int neededNumber = backgroundColorRanges.size(); - int availableNumber = backgroundShapes.size(); + int neededNumber = rangeList.size(); + int availableNumber = shapeList.size(); if (neededNumber < availableNumber) { - List unusedShapes = backgroundShapes.subList(neededNumber, availableNumber); + List unusedShapes = shapeList.subList(neededNumber, availableNumber); getChildren().removeAll(unusedShapes); unusedShapes.clear(); } else if (availableNumber < neededNumber) { for (int i = 0; i < neededNumber - availableNumber; i++) { - Path backgroundShape = new Path(); - backgroundShape.setManaged(false); - backgroundShape.setStrokeWidth(0); - backgroundShape.layoutXProperty().bind(leftInset); - backgroundShape.layoutYProperty().bind(topInset); - - backgroundShapes.add(backgroundShape); - getChildren().add(0, backgroundShape); + Path shape = new Path(); + shape.setManaged(false); + shape.layoutXProperty().bind(leftInset); + shape.layoutYProperty().bind(topInset); + + shapeList.add(shape); + addToChildren.accept(getChildren(), shape); } } // update the shape's color and elements - int i = 0; - for (Tuple2 t : backgroundColorRanges) { - Path backgroundShape = backgroundShapes.get(i); - backgroundShape.setFill(t._1); - backgroundShape.getElements().setAll(getRangeShape(t._2)); - i++; + for (int i = 0; i < rangeList.size(); i++) { + configureShape.accept(shapeList.get(i), rangeList.get(i)); } // clear, since it's no longer needed - backgroundColorRanges.clear(); + rangeList.clear(); } - private Path getUnderlineShape(int index) { - Path underlineShape = underlineShapes.get(index); - if (underlineShape == null) { - // add corresponding underline node (empty) - underlineShape = new Path(); - underlineShape.setManaged(false); - underlineShape.setStrokeWidth(0); - underlineShape.layoutXProperty().bind(leftInset); - underlineShape.layoutYProperty().bind(topInset); - underlineShapes.set(index, underlineShape); - getChildren().add(underlineShape); - } - return underlineShape; + + @Override + protected void layoutChildren() { + super.layoutChildren(); + updateCaretShape(); + updateSelectionShape(); + updateBackgroundShapes(); } - /** - * Updates the shape which renders the text underline. - * - * @param text The text node which specified the style attributes - * @param start The index of the first character - * @param end The index of the last character - * @param index The index of the background shape - */ - private void updateUnderline(TextExt text, int start, int end, int index) { - - Number underlineWidth = text.underlineWidthProperty().get(); - if (underlineWidth != null && underlineWidth.doubleValue() > 0) { - - Path underlineShape = getUnderlineShape(index); - underlineShape.setStrokeWidth(underlineWidth.doubleValue()); - - // get remaining CSS properties for the underline style - - Paint underlineColor = text.underlineColorProperty().get(); - - // get the dash array - JavaFX CSS parser seems to return either a Number[] array - // or a single value, depending on whether only one or more than one value has been - // specified in the CSS - Double[] underlineDashArray = null; - Object underlineDashArrayProp = text.underlineDashArrayProperty().get(); - if (underlineDashArrayProp != null) { - if (underlineDashArrayProp.getClass().isArray()) { - Number[] numberArray = (Number[]) underlineDashArrayProp; - underlineDashArray = new Double[numberArray.length]; - int idx = 0; - for (Number d : numberArray) { - underlineDashArray[idx++] = (Double) d; + private static class UnderlineAttributes { + + private final double width; + private final Paint color; + private final Double[] dashArray; + private final StrokeLineCap cap; + + public final boolean isNullValue() { return color == null || width == -1; } + + UnderlineAttributes(TextExt text) { + color = text.getUnderlineColor(); + Number underlineWidth = text.getUnderlineWidth(); + if (color == null || underlineWidth == null || underlineWidth.doubleValue() <= 0) { + // null value + width = -1; + dashArray = null; + cap = null; + } else { + // real value + width = underlineWidth.doubleValue(); + cap = text.getUnderlineCap(); + + // get the dash array - JavaFX CSS parser seems to return either a Number[] array + // or a single value, depending on whether only one or more than one value has been + // specified in the CSS + Object underlineDashArrayProp = text.underlineDashArrayProperty().get(); + if (underlineDashArrayProp != null) { + if (underlineDashArrayProp.getClass().isArray()) { + Number[] numberArray = (Number[]) underlineDashArrayProp; + dashArray = new Double[numberArray.length]; + int idx = 0; + for (Number d : numberArray) { + dashArray[idx++] = (Double) d; + } + } else { + dashArray = new Double[1]; + dashArray[0] = ((Double) underlineDashArrayProp).doubleValue(); } } else { - underlineDashArray = new Double[1]; - underlineDashArray[0] = ((Double) underlineDashArrayProp).doubleValue(); + dashArray = null; } } - - StrokeLineCap underlineCap = text.underlineCapProperty().get(); - - // apply style - if (underlineColor != null) { - underlineShape.setStroke(underlineColor); - } - if (underlineDashArray != null) { - underlineShape.getStrokeDashArray().addAll(underlineDashArray); - } - if (underlineCap != null) { - underlineShape.setStrokeLineCap(underlineCap); - } - - // Set path elements - PathElement[] shape = getUnderlineShape(start, end); - underlineShape.getElements().setAll(shape); } - } - + @Override + public boolean equals(Object obj) { + if (obj instanceof UnderlineAttributes) { + UnderlineAttributes attr = (UnderlineAttributes) obj; + return Objects.equals(width, attr.width) + && Objects.equals(color, attr.color) + && Objects.equals(cap, attr.cap) + && Arrays.equals(dashArray, attr.dashArray); + } else { + return false; + } + } - @Override - protected void layoutChildren() { - super.layoutChildren(); - updateCaretShape(); - updateSelectionShape(); - updateBackgroundShapes(); + @Override + public String toString() { + return String.format("UnderlineAttributes[width=%s color=%s cap=%s dashArray=%s", width, color, cap, Arrays.toString(dashArray)); + } } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java b/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java index 38b16c9c5..fdd4dd413 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java @@ -98,6 +98,10 @@ PathElement[] getRangeShape(int from, int to) { return textLayout().getRange(from, to, TextLayout.TYPE_TEXT, 0, 0); } + PathElement[] getUnderlineShape(IndexRange range) { + return getUnderlineShape(range.getStart(), range.getEnd()); + } + /** * @param from The index of the first character. * @param to The index of the last character.