|
25 | 25 | import javafx.beans.property.SimpleObjectProperty; |
26 | 26 | import javafx.beans.value.ObservableValue; |
27 | 27 | import javafx.collections.FXCollections; |
| 28 | +import javafx.collections.ListChangeListener.Change; |
28 | 29 | import javafx.collections.ObservableSet; |
29 | 30 | import javafx.css.CssMetaData; |
30 | 31 | import javafx.css.PseudoClass; |
|
47 | 48 | import javafx.scene.layout.Region; |
48 | 49 | import javafx.scene.paint.Color; |
49 | 50 | import javafx.scene.paint.Paint; |
| 51 | +import javafx.scene.shape.LineTo; |
| 52 | +import javafx.scene.shape.PathElement; |
50 | 53 | import javafx.scene.text.TextFlow; |
51 | 54 |
|
52 | 55 | import org.fxmisc.flowless.Cell; |
@@ -1091,14 +1094,98 @@ public void requestFollowCaret() { |
1091 | 1094 |
|
1092 | 1095 | @Override |
1093 | 1096 | public void lineStart(SelectionPolicy policy) { |
1094 | | - int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineStartPosition(caretSelectionBind.getUnderlyingCaret()); |
1095 | | - moveTo(getCurrentParagraph(), columnPos, policy); |
| 1097 | + moveTo(getCurrentParagraph(), getCurrentLineStartInParargraph(), policy); |
1096 | 1098 | } |
1097 | 1099 |
|
1098 | 1100 | @Override |
1099 | 1101 | public void lineEnd(SelectionPolicy policy) { |
1100 | | - int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineEndPosition(caretSelectionBind.getUnderlyingCaret()); |
1101 | | - moveTo(getCurrentParagraph(), columnPos, policy); |
| 1102 | + moveTo(getCurrentParagraph(), getCurrentLineEndInParargraph(), policy); |
| 1103 | + } |
| 1104 | + |
| 1105 | + public int getCurrentLineStartInParargraph() { |
| 1106 | + return virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineStartPosition(caretSelectionBind.getUnderlyingCaret()); |
| 1107 | + } |
| 1108 | + |
| 1109 | + public int getCurrentLineEndInParargraph() { |
| 1110 | + return virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineEndPosition(caretSelectionBind.getUnderlyingCaret()); |
| 1111 | + } |
| 1112 | + |
| 1113 | + private double caretPrevY = -1; |
| 1114 | + private Selection<PS, SEG, S> lineHighlighter; |
| 1115 | + private ObjectProperty<Paint> lineHighlighterFill; |
| 1116 | + |
| 1117 | + /** |
| 1118 | + * The default fill is "highlighter" yellow. It can also be styled using CSS with:<br> |
| 1119 | + * <code>.styled-text-area .line-highlighter { -fx-fill: lime; }</code><br> |
| 1120 | + * CSS selectors from Path, Shape, and Node can also be used. |
| 1121 | + */ |
| 1122 | + public void setLineHighlighterFill( Paint highlight ) |
| 1123 | + { |
| 1124 | + if ( lineHighlighterFill != null && highlight != null ) { |
| 1125 | + lineHighlighterFill.set( highlight ); |
| 1126 | + } |
| 1127 | + else { |
| 1128 | + boolean lineHighlightOn = isLineHighlighterOn(); |
| 1129 | + if ( lineHighlightOn ) setLineHighlighterOn( false ); |
| 1130 | + |
| 1131 | + if ( highlight == null ) lineHighlighterFill = null; |
| 1132 | + else lineHighlighterFill = new SimpleObjectProperty( highlight ); |
| 1133 | + |
| 1134 | + if ( lineHighlightOn ) setLineHighlighterOn( true ); |
| 1135 | + } |
| 1136 | + } |
| 1137 | + |
| 1138 | + public boolean isLineHighlighterOn() { |
| 1139 | + return lineHighlighter != null && selectionSet.contains( lineHighlighter ) ; |
| 1140 | + } |
| 1141 | + |
| 1142 | + /** |
| 1143 | + * Highlights the line that the main caret is on.<br> |
| 1144 | + * Line highlighting automatically follows the caret. |
| 1145 | + */ |
| 1146 | + public void setLineHighlighterOn( boolean show ) |
| 1147 | + { |
| 1148 | + if ( show ) |
| 1149 | + { |
| 1150 | + if ( lineHighlighter != null ) return; |
| 1151 | + |
| 1152 | + lineHighlighter = new SelectionImpl<>( "line-highlighter", this, path -> |
| 1153 | + { |
| 1154 | + if ( lineHighlighterFill == null ) path.setHighlightFill( Color.YELLOW ); |
| 1155 | + else path.highlightFillProperty().bind( lineHighlighterFill ); |
| 1156 | + |
| 1157 | + path.getElements().addListener( (Change<? extends PathElement> chg) -> |
| 1158 | + { |
| 1159 | + if ( chg.next() && chg.wasAdded() || chg.wasReplaced() ) { |
| 1160 | + double maxX = getLayoutBounds().getMaxX(); |
| 1161 | + // The path is limited to the bounds of the text, so here it's altered to the area's width |
| 1162 | + chg.getAddedSubList().stream().skip(1).limit(2).forEach( ele -> ((LineTo) ele).setX( maxX ) ); |
| 1163 | + // The path can wrap onto another line if enough text is inserted, so here it's trimmed |
| 1164 | + if ( chg.getAddedSize() > 5 ) path.getElements().remove( 5, 10 ); |
| 1165 | + } |
| 1166 | + } ); |
| 1167 | + } ); |
| 1168 | + |
| 1169 | + Consumer<Bounds> caretListener = b -> |
| 1170 | + { |
| 1171 | + if ( b.getMinY() != caretPrevY && lineHighlighter != null ) |
| 1172 | + { |
| 1173 | + int p = getCurrentParagraph(); |
| 1174 | + lineHighlighter.selectRange( p, getCurrentLineStartInParargraph(), p, getCurrentLineEndInParargraph() ); |
| 1175 | + caretPrevY = b.getMinY(); |
| 1176 | + } |
| 1177 | + }; |
| 1178 | + |
| 1179 | + caretBoundsProperty().addListener( (ob,ov,nv) -> nv.ifPresent( caretListener ) ); |
| 1180 | + getCaretBounds().ifPresent( caretListener ); |
| 1181 | + selectionSet.add( lineHighlighter ); |
| 1182 | + } |
| 1183 | + else if ( lineHighlighter != null ) { |
| 1184 | + selectionSet.remove( lineHighlighter ); |
| 1185 | + lineHighlighter.deselect(); |
| 1186 | + lineHighlighter = null; |
| 1187 | + caretPrevY = -1; |
| 1188 | + } |
1102 | 1189 | } |
1103 | 1190 |
|
1104 | 1191 | @Override |
@@ -1447,6 +1534,7 @@ private Cell<Paragraph<PS, SEG, S>, ParagraphBox<PS, SEG, S>> createCell( |
1447 | 1534 | selection.rangeProperty() |
1448 | 1535 | ); |
1449 | 1536 | SelectionPath path = new SelectionPath(range); |
| 1537 | + path.getStyleClass().add( selection.getSelectionName() ); |
1450 | 1538 | selection.configureSelectionPath(path); |
1451 | 1539 |
|
1452 | 1540 | box.selectionsProperty().put(selection, path); |
|
0 commit comments