diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index 218f1ed37..53dcafa31 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -25,6 +25,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableSet; import javafx.css.CssMetaData; import javafx.css.PseudoClass; @@ -47,6 +48,8 @@ import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.PathElement; import javafx.scene.text.TextFlow; import org.fxmisc.flowless.Cell; @@ -1091,14 +1094,98 @@ public void requestFollowCaret() { @Override public void lineStart(SelectionPolicy policy) { - int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineStartPosition(caretSelectionBind.getUnderlyingCaret()); - moveTo(getCurrentParagraph(), columnPos, policy); + moveTo(getCurrentParagraph(), getCurrentLineStartInParargraph(), policy); } @Override public void lineEnd(SelectionPolicy policy) { - int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineEndPosition(caretSelectionBind.getUnderlyingCaret()); - moveTo(getCurrentParagraph(), columnPos, policy); + moveTo(getCurrentParagraph(), getCurrentLineEndInParargraph(), policy); + } + + public int getCurrentLineStartInParargraph() { + return virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineStartPosition(caretSelectionBind.getUnderlyingCaret()); + } + + public int getCurrentLineEndInParargraph() { + return virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineEndPosition(caretSelectionBind.getUnderlyingCaret()); + } + + private double caretPrevY = -1; + private Selection lineHighlighter; + private ObjectProperty lineHighlighterFill; + + /** + * The default fill is "highlighter" yellow. It can also be styled using CSS with:
+ * .styled-text-area .line-highlighter { -fx-fill: lime; }
+ * CSS selectors from Path, Shape, and Node can also be used. + */ + public void setLineHighlighterFill( Paint highlight ) + { + if ( lineHighlighterFill != null && highlight != null ) { + lineHighlighterFill.set( highlight ); + } + else { + boolean lineHighlightOn = isLineHighlighterOn(); + if ( lineHighlightOn ) setLineHighlighterOn( false ); + + if ( highlight == null ) lineHighlighterFill = null; + else lineHighlighterFill = new SimpleObjectProperty( highlight ); + + if ( lineHighlightOn ) setLineHighlighterOn( true ); + } + } + + public boolean isLineHighlighterOn() { + return lineHighlighter != null && selectionSet.contains( lineHighlighter ) ; + } + + /** + * Highlights the line that the main caret is on.
+ * Line highlighting automatically follows the caret. + */ + public void setLineHighlighterOn( boolean show ) + { + if ( show ) + { + if ( lineHighlighter != null ) return; + + lineHighlighter = new SelectionImpl<>( "line-highlighter", this, path -> + { + if ( lineHighlighterFill == null ) path.setHighlightFill( Color.YELLOW ); + else path.highlightFillProperty().bind( lineHighlighterFill ); + + path.getElements().addListener( (Change chg) -> + { + if ( chg.next() && chg.wasAdded() || chg.wasReplaced() ) { + double maxX = getLayoutBounds().getMaxX(); + // The path is limited to the bounds of the text, so here it's altered to the area's width + chg.getAddedSubList().stream().skip(1).limit(2).forEach( ele -> ((LineTo) ele).setX( maxX ) ); + // The path can wrap onto another line if enough text is inserted, so here it's trimmed + if ( chg.getAddedSize() > 5 ) path.getElements().remove( 5, 10 ); + } + } ); + } ); + + Consumer caretListener = b -> + { + if ( b.getMinY() != caretPrevY && lineHighlighter != null ) + { + int p = getCurrentParagraph(); + lineHighlighter.selectRange( p, getCurrentLineStartInParargraph(), p, getCurrentLineEndInParargraph() ); + caretPrevY = b.getMinY(); + } + }; + + caretBoundsProperty().addListener( (ob,ov,nv) -> nv.ifPresent( caretListener ) ); + getCaretBounds().ifPresent( caretListener ); + selectionSet.add( lineHighlighter ); + } + else if ( lineHighlighter != null ) { + selectionSet.remove( lineHighlighter ); + lineHighlighter.deselect(); + lineHighlighter = null; + caretPrevY = -1; + } } @Override @@ -1447,6 +1534,7 @@ private Cell, ParagraphBox> createCell( selection.rangeProperty() ); SelectionPath path = new SelectionPath(range); + path.getStyleClass().add( selection.getSelectionName() ); selection.configureSelectionPath(path); box.selectionsProperty().put(selection, path); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/Selection.java b/richtextfx/src/main/java/org/fxmisc/richtext/Selection.java index 791da8115..1739cbcd2 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/Selection.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/Selection.java @@ -189,7 +189,9 @@ default void deselect() { void dispose(); /** - * Gets the name of this selection. Each selection in an area must have a unique name. + * Gets the name of this selection. Each selection in an area must have a unique name.
+ * The name is also used as a StyleClass, so the Selection can be styled using CSS selectors + * from Path, Shape, and Node eg:
.styled-text-area .my-selection { -fx-fill: lime; } */ String getSelectionName(); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/SelectionImpl.java b/richtextfx/src/main/java/org/fxmisc/richtext/SelectionImpl.java index e3f6ff808..f70058847 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/SelectionImpl.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/SelectionImpl.java @@ -116,6 +116,8 @@ public class SelectionImpl implements Selection, Compara /** * Creates a selection with both the start and end position at 0. + * @param name must be unique and is also used as a StyleClass for + * configuration via CSS using selectors from Path, Shape, and Node. */ public SelectionImpl(String name, GenericStyledArea area) { this(name, area, 0, 0); @@ -130,7 +132,8 @@ public SelectionImpl(String name, GenericStyledArea area, Consumer area, int startPosition, int endPosition) { this(name, area, new IndexRange(startPosition, endPosition), area.beingUpdatedProperty());