1
+ package org .fxmisc .richtext ;
2
+
3
+ import static org .fxmisc .richtext .model .TwoDimensional .Bias .*;
4
+
5
+ import java .util .ArrayList ;
6
+ import java .util .List ;
7
+
8
+ import javafx .geometry .Point2D ;
9
+ import javafx .geometry .Rectangle2D ;
10
+ import javafx .scene .control .IndexRange ;
11
+ import org .fxmisc .richtext .model .TwoLevelNavigator ;
12
+
13
+ import javafx .scene .shape .PathElement ;
14
+ import javafx .scene .text .HitInfo ;
15
+ import javafx .scene .text .TextFlow ;
16
+ import javafx .scene .shape .LineTo ;
17
+ import javafx .scene .shape .MoveTo ;
18
+
19
+ /**
20
+ * Adds additional API to {@link TextFlow}.
21
+ */
22
+ class TextFlowExt extends TextFlow {
23
+
24
+ private TextFlowLayout layout ;
25
+
26
+ private TextFlowLayout textLayout ()
27
+ {
28
+ if ( layout == null ) {
29
+ layout = new TextFlowLayout ( this , getManagedChildren () );
30
+ }
31
+ return layout ;
32
+ }
33
+
34
+ int getLineCount () {
35
+ return textLayout ().getLineCount ();
36
+ }
37
+
38
+ int getLineStartPosition (int charIdx ) {
39
+ TwoLevelNavigator navigator = textLayout ().getTwoLevelNavigator ();
40
+ int currentLineIndex = navigator .offsetToPosition (charIdx , Forward ).getMajor ();
41
+ return navigator .position (currentLineIndex , 0 ).toOffset ();
42
+ }
43
+
44
+ int getLineEndPosition (int charIdx ) {
45
+ TwoLevelNavigator navigator = textLayout ().getTwoLevelNavigator ();
46
+ int currentLineIndex = navigator .offsetToPosition (charIdx , Forward ).getMajor ();
47
+ int minor = (currentLineIndex == getLineCount () - 1 ) ? 0 : -1 ;
48
+ return navigator .position (currentLineIndex + 1 , minor ).toOffset ();
49
+ }
50
+
51
+ int getLineOfCharacter (int charIdx ) {
52
+ TwoLevelNavigator navigator = textLayout ().getTwoLevelNavigator ();
53
+ return navigator .offsetToPosition (charIdx , Forward ).getMajor ();
54
+ }
55
+
56
+ PathElement [] getCaretShape (int charIdx , boolean isLeading ) {
57
+ return caretShape (charIdx , isLeading );
58
+ }
59
+
60
+ PathElement [] getRangeShape (IndexRange range ) {
61
+ return getRangeShape (range .getStart (), range .getEnd ());
62
+ }
63
+
64
+ PathElement [] getRangeShape (int from , int to ) {
65
+ return rangeShape (from , to );
66
+ }
67
+
68
+ PathElement [] getUnderlineShape (IndexRange range ) {
69
+ return getUnderlineShape (range .getStart (), range .getEnd ());
70
+ }
71
+
72
+ /**
73
+ * @param from The index of the first character.
74
+ * @param to The index of the last character.
75
+ * @return An array with the PathElement objects which define an
76
+ * underline from the first to the last character.
77
+ */
78
+ PathElement [] getUnderlineShape (int from , int to ) {
79
+ // get a Path for the text underline
80
+ List <PathElement > result = new ArrayList <>();
81
+
82
+ PathElement [] shape = rangeShape ( from , to );
83
+ // The shape is a closed Path for one or more rectangles AROUND the selected text.
84
+ // shape: [MoveTo origin, LineTo top R, LineTo bottom R, LineTo bottom L, LineTo origin, *]
85
+
86
+ // Extract the bottom left and right coordinates for each rectangle to get the underline path.
87
+ for ( int ele = 2 ; ele < shape .length ; ele += 5 )
88
+ {
89
+ LineTo bl = (LineTo ) shape [ele +1 ];
90
+ LineTo br = (LineTo ) shape [ele ];
91
+ double y = br .getY () - 2.5 ;
92
+
93
+ result .add ( new MoveTo ( bl .getX (), y ) );
94
+ result .add ( new LineTo ( br .getX (), y ) );
95
+ }
96
+
97
+ return result .toArray (new PathElement [0 ]);
98
+ }
99
+
100
+ CharacterHit hitLine (double x , int lineIndex ) {
101
+ return hit (x , textLayout ().getLineCenter ( lineIndex ));
102
+ }
103
+
104
+ CharacterHit hit (double x , double y ) {
105
+ TextFlowSpan span = textLayout ().getLineSpan ( (float ) y );
106
+ Rectangle2D lineBounds = span .getBounds ();
107
+
108
+ HitInfo hit = hitTest (new Point2D (x , y ));
109
+ int charIdx = hit .getCharIndex ();
110
+ boolean leading = hit .isLeading ();
111
+
112
+ if ( ! leading ) {
113
+ // If this is a wrapped paragraph and hit character is at end of hit line, make sure that the
114
+ // "character hit" stays at the end of the hit line (and not at the beginning of the next line).
115
+ leading = (getLineCount () > 1 && charIdx + 1 >= span .getStart () + span .getLength ());
116
+ }
117
+
118
+ if (x < lineBounds .getMinX () || x > lineBounds .getMaxX ()) {
119
+ if (leading ) {
120
+ return CharacterHit .insertionAt (charIdx );
121
+ } else {
122
+ return CharacterHit .insertionAt (charIdx + 1 );
123
+ }
124
+ } else {
125
+ if (leading ) {
126
+ return CharacterHit .leadingHalfOf (charIdx );
127
+ } else {
128
+ return CharacterHit .trailingHalfOf (charIdx );
129
+ }
130
+ }
131
+ }
132
+
133
+ }
0 commit comments