6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package com.sun.javafx.scene.control.skin;
27
28 import javafx.beans.binding.BooleanBinding;
29 import javafx.beans.binding.DoubleBinding;
30 import javafx.beans.binding.ObjectBinding;
31 import javafx.beans.binding.StringBinding;
32 import javafx.beans.property.DoubleProperty;
33 import javafx.beans.property.SimpleDoubleProperty;
34 import javafx.beans.value.ObservableBooleanValue;
35 import javafx.beans.value.ObservableDoubleValue;
36 import javafx.event.EventHandler;
37 import javafx.geometry.Bounds;
38 import javafx.geometry.HPos;
39 import javafx.geometry.Point2D;
40 import javafx.geometry.Rectangle2D;
41 import javafx.scene.AccessibleAttribute;
42 import javafx.scene.Group;
43 import javafx.scene.Node;
44 import javafx.scene.control.IndexRange;
45 import javafx.scene.control.PasswordField;
46 import javafx.scene.control.TextField;
47 import javafx.scene.input.MouseEvent;
48 import javafx.scene.layout.Pane;
49 import javafx.scene.paint.Color;
50 import javafx.scene.paint.Paint;
51 import javafx.scene.shape.Path;
52 import javafx.scene.shape.PathElement;
53 import javafx.scene.shape.Rectangle;
54 import javafx.scene.text.Text;
55 import java.util.List;
56 import com.sun.javafx.scene.control.behavior.TextFieldBehavior;
57 import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior;
58 import com.sun.javafx.scene.text.HitInfo;
59
60 /**
61 * Text field skin.
62 */
63 public class TextFieldSkin extends TextInputControlSkin<TextField, TextFieldBehavior> {
64 /**
65 * This group contains the text, caret, and selection rectangle.
66 * It is clipped. The textNode, selectionHighlightPath, and
67 * caret are each translated individually when horizontal
68 * translation is needed to keep the caretPosition visible.
69 */
70 private Pane textGroup = new Pane();
71 private Group handleGroup;
72
73 /**
74 * The clip, applied to the textGroup. This makes sure that any
75 * text / selection wandering off the text box is clipped
76 */
77 private Rectangle clip = new Rectangle();
78 /**
79 * The node actually displaying the text. Note that it has the
80 * ability to render both the normal fill as well as the highlight
81 * fill, to perform hit testing, fetching of the selection
82 * highlight, and other such duties.
83 */
85 /**
86 *
87 * The node used for showing the prompt text.
88 */
89 private Text promptNode;
90 /**
91 * A path, provided by the textNode, which represents the area
92 * which is selected. The path elements which make up the
93 * selection must be updated whenever the selection changes. We
94 * don't need to keep track of text changes because those will
95 * force the selection to be updated.
96 */
97 private Path selectionHighlightPath = new Path();
98
99 private Path characterBoundingPath = new Path();
100 private ObservableBooleanValue usePromptText;
101 private DoubleProperty textTranslateX = new SimpleDoubleProperty(this, "textTranslateX");
102 private double caretWidth;
103
104 /**
105 * Function to translate the text control's "dot" into the caret
106 * position in the Text node. This is possibly only meaningful for
107 * the PasswordField where the echoChar could be more than one
108 * character.
109 */
110 protected int translateCaretPosition(int cp) { return cp; }
111 protected Point2D translateCaretPosition(Point2D p) { return p; }
112
113 /**
114 * Right edge of the text region sans padding
115 */
116 protected ObservableDoubleValue textRight;
117
118 private double pressX, pressY; // For dragging handles on embedded
119
120 // For use with PasswordField
121 public static final char BULLET = '\u25cf';
122
123 /**
124 * Create a new TextFieldSkin.
125 * @param textField not null
126 */
127 public TextFieldSkin(final TextField textField) {
128 this(textField, (textField instanceof PasswordField)
129 ? new PasswordFieldBehavior((PasswordField)textField)
130 : new TextFieldBehavior(textField));
131 }
132
133 public TextFieldSkin(final TextField textField, final TextFieldBehavior behavior) {
134 super(textField, behavior);
135 behavior.setTextFieldSkin(this);
136
137
138 textField.caretPositionProperty().addListener((observable, oldValue, newValue) -> {
139 if (textField.getWidth() > 0) {
140 updateTextNodeCaretPos(textField.getCaretPosition());
141 if (!isForwardBias()) {
142 setForwardBias(true);
143 }
144 updateCaretOff();
145 }
146 });
147
148 forwardBiasProperty().addListener(observable -> {
149 if (textField.getWidth() > 0) {
150 updateTextNodeCaretPos(textField.getCaretPosition());
151 updateCaretOff();
152 }
153 });
154
155 textRight = new DoubleBinding() {
156 { bind(textGroup.widthProperty()); }
157 @Override protected double computeValue() {
158 return textGroup.getWidth();
159 }
160 };
161
162 // Once this was crucial for performance, not sure now.
163 clip.setSmooth(false);
164 clip.setX(0);
165 clip.widthProperty().bind(textGroup.widthProperty());
166 clip.heightProperty().bind(textGroup.heightProperty());
167
168 // Add content
169 textGroup.setClip(clip);
170 // Hack to defeat the fact that otherwise when the caret blinks the parent group
171 // bounds are completely invalidated and therefore the dirty region is much
172 // larger than necessary.
173 textGroup.getChildren().addAll(selectionHighlightPath, textNode, new Group(caretPath));
174 getChildren().add(textGroup);
175 if (SHOW_HANDLES) {
176 handleGroup = new Group();
177 handleGroup.setManaged(false);
178 handleGroup.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2);
179 getChildren().add(handleGroup);
180 }
181
182 // Add text
183 textNode.setManaged(false);
184 textNode.getStyleClass().add("text");
185 textNode.fontProperty().bind(textField.fontProperty());
186
187 textNode.layoutXProperty().bind(textTranslateX);
188 textNode.textProperty().bind(new StringBinding() {
189 { bind(textField.textProperty()); }
190 @Override protected String computeValue() {
191 return maskText(textField.textProperty().getValueSafe());
192 }
193 });
194 textNode.fillProperty().bind(textFill);
195 textNode.impl_selectionFillProperty().bind(new ObjectBinding<Paint>() {
196 { bind(highlightTextFill, textFill, textField.focusedProperty()); }
197 @Override protected Paint computeValue() {
198 return textField.isFocused() ? highlightTextFill.get() : textFill.get();
199 }
200 });
201 // updated by listener on caretPosition to ensure order
202 updateTextNodeCaretPos(textField.getCaretPosition());
203 textField.selectionProperty().addListener(observable -> {
204 updateSelection();
205 });
206
207 // Add selection
208 selectionHighlightPath.setManaged(false);
209 selectionHighlightPath.setStroke(null);
210 selectionHighlightPath.layoutXProperty().bind(textTranslateX);
211 selectionHighlightPath.visibleProperty().bind(textField.anchorProperty().isNotEqualTo(textField.caretPositionProperty()).and(textField.focusedProperty()));
212 selectionHighlightPath.fillProperty().bind(highlightFill);
213 textNode.impl_selectionShapeProperty().addListener(observable -> {
214 updateSelection();
215 });
216
217 // Add caret
218 caretPath.setManaged(false);
219 caretPath.setStrokeWidth(1);
220 caretPath.fillProperty().bind(textFill);
221 caretPath.strokeProperty().bind(textFill);
222
223 // modifying visibility of the caret forces a layout-pass (RT-32373), so
224 // instead we modify the opacity.
225 caretPath.opacityProperty().bind(new DoubleBinding() {
226 { bind(caretVisible); }
227 @Override protected double computeValue() {
228 return caretVisible.get() ? 1.0 : 0.0;
229 }
230 });
231 caretPath.layoutXProperty().bind(textTranslateX);
232 textNode.impl_caretShapeProperty().addListener(observable -> {
233 caretPath.getElements().setAll(textNode.impl_caretShapeProperty().get());
234 if (caretPath.getElements().size() == 0) {
235 // The caret pos is invalid.
236 updateTextNodeCaretPos(textField.getCaretPosition());
237 } else if (caretPath.getElements().size() == 4) {
238 // The caret is split. Ignore and keep the previous width value.
239 } else {
240 caretWidth = Math.round(caretPath.getLayoutBounds().getWidth());
241 }
242 });
243
244 // Be sure to get the control to request layout when the font changes,
245 // since this will affect the pref height and pref width.
246 textField.fontProperty().addListener(observable -> {
247 // I do both so that any cached values for prefWidth/height are cleared.
248 // The problem is that the skin is unmanaged and so calling request layout
249 // doesn't walk up the tree all the way. I think....
250 textField.requestLayout();
251 getSkinnable().requestLayout();
252 });
253
254 registerChangeListener(textField.prefColumnCountProperty(), "prefColumnCount");
255 if (textField.isFocused()) setCaretAnimating(true);
256
257 textField.alignmentProperty().addListener(observable -> {
258 if (textField.getWidth() > 0) {
259 updateTextPos();
260 updateCaretOff();
261 textField.requestLayout();
262 }
263 });
264
265 usePromptText = new BooleanBinding() {
266 { bind(textField.textProperty(),
267 textField.promptTextProperty(),
268 promptTextFill); }
269 @Override protected boolean computeValue() {
270 String txt = textField.getText();
271 String promptTxt = textField.getPromptText();
272 return ((txt == null || txt.isEmpty()) &&
273 promptTxt != null && !promptTxt.isEmpty() &&
274 !promptTextFill.get().equals(Color.TRANSPARENT));
275 }
276 };
277
278 promptTextFill.addListener(observable -> {
279 updateTextPos();
280 });
281
282 textField.textProperty().addListener(observable -> {
283 if (!getBehavior().isEditing()) {
284 // Text changed, but not by user action
285 updateTextPos();
286 }
287 });
288
289 if (usePromptText.get()) {
290 createPromptNode();
291 }
292
293 usePromptText.addListener(observable -> {
294 createPromptNode();
295 textField.requestLayout();
296 });
297
298 if (SHOW_HANDLES) {
299 selectionHandle1.setRotate(180);
300
301 EventHandler<MouseEvent> handlePressHandler = e -> {
302 pressX = e.getX();
303 pressY = e.getY();
304 e.consume();
305 };
306
307 caretHandle.setOnMousePressed(handlePressHandler);
308 selectionHandle1.setOnMousePressed(handlePressHandler);
309 selectionHandle2.setOnMousePressed(handlePressHandler);
310
311 caretHandle.setOnMouseDragged(e -> {
312 Point2D p = new Point2D(caretHandle.getLayoutX() + e.getX() + pressX - textNode.getLayoutX(),
313 caretHandle.getLayoutY() + e.getY() - pressY - 6);
314 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
315 positionCaret(hit, false);
316 e.consume();
317 });
318
319 selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() {
320 @Override public void handle(MouseEvent e) {
321 TextField textField = getSkinnable();
322 Point2D tp = textNode.localToScene(0, 0);
323 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2,
324 e.getSceneY() - tp.getY() - pressY - 6);
325 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
326 int pos = hit.getCharIndex();
327 if (textField.getAnchor() < textField.getCaretPosition()) {
328 // Swap caret and anchor
329 textField.selectRange(textField.getCaretPosition(), textField.getAnchor());
330 }
331 if (pos >= 0) {
332 if (pos >= textField.getAnchor() - 1) {
333 hit.setCharIndex(Math.max(0, textField.getAnchor() - 1));
334 }
335 positionCaret(hit, true);
336 }
337 e.consume();
338 }
339 });
340
341 selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() {
342 @Override public void handle(MouseEvent e) {
343 TextField textField = getSkinnable();
344 Point2D tp = textNode.localToScene(0, 0);
345 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2,
346 e.getSceneY() - tp.getY() - pressY - 6);
347 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
348 int pos = hit.getCharIndex();
349 if (textField.getAnchor() > textField.getCaretPosition()) {
350 // Swap caret and anchor
351 textField.selectRange(textField.getCaretPosition(), textField.getAnchor());
352 }
353 if (pos > 0) {
354 if (pos <= textField.getAnchor()) {
355 hit.setCharIndex(Math.min(textField.getAnchor() + 1, textField.getLength()));
356 }
357 positionCaret(hit, true);
358 }
359 e.consume();
360 }
361 });
362 }
363 }
364
365 private void updateTextNodeCaretPos(int pos) {
366 if (pos == 0 || isForwardBias()) {
367 textNode.setImpl_caretPosition(pos);
368 } else {
369 textNode.setImpl_caretPosition(pos - 1);
370 }
371 textNode.impl_caretBiasProperty().set(isForwardBias());
372 }
373
374 private void createPromptNode() {
375 if (promptNode != null || !usePromptText.get()) return;
376
377 promptNode = new Text();
378 textGroup.getChildren().add(0, promptNode);
379 promptNode.setManaged(false);
380 promptNode.getStyleClass().add("text");
381 promptNode.visibleProperty().bind(usePromptText);
382 promptNode.fontProperty().bind(getSkinnable().fontProperty());
383
384 promptNode.textProperty().bind(getSkinnable().promptTextProperty());
385 promptNode.fillProperty().bind(promptTextFill);
386 updateSelection();
387 }
388
389 private void updateSelection() {
390 TextField textField = getSkinnable();
391 IndexRange newValue = textField.getSelection();
392
393 if (newValue == null || newValue.getLength() == 0) {
394 textNode.impl_selectionStartProperty().set(-1);
395 textNode.impl_selectionEndProperty().set(-1);
396 } else {
397 textNode.impl_selectionStartProperty().set(newValue.getStart());
398 // This intermediate value is needed to force selection shape layout.
399 textNode.impl_selectionEndProperty().set(newValue.getStart());
400 textNode.impl_selectionEndProperty().set(newValue.getEnd());
401 }
402
403 PathElement[] elements = textNode.impl_selectionShapeProperty().get();
404 if (elements == null) {
405 selectionHighlightPath.getElements().clear();
406 } else {
407 selectionHighlightPath.getElements().setAll(elements);
408 }
409
410 if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) {
411 int caretPos = textField.getCaretPosition();
412 int anchorPos = textField.getAnchor();
413
414 {
415 // Position the handle for the anchor. This could be handle1 or handle2.
416 // Do this before positioning the handle for the caret.
417 updateTextNodeCaretPos(anchorPos);
418 Bounds b = caretPath.getBoundsInParent();
419 if (caretPos < anchorPos) {
420 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
421 } else {
422 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
423 }
424 }
425
426 {
427 // Position handle for the caret. This could be handle1 or handle2.
428 updateTextNodeCaretPos(caretPos);
429 Bounds b = caretPath.getBoundsInParent();
430 if (caretPos < anchorPos) {
431 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
432 } else {
433 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
434 }
435 }
436 }
437 }
438
439 @Override protected void handleControlPropertyChanged(String propertyReference) {
440 if ("prefColumnCount".equals(propertyReference)) {
441 getSkinnable().requestLayout();
442 } else {
443 super.handleControlPropertyChanged(propertyReference);
444 }
445 }
446
447 @Override
448 protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
449 TextField textField = getSkinnable();
450
451 double characterWidth = fontMetrics.get().computeStringWidth("W");
452
453 int columnCount = textField.getPrefColumnCount();
454
455 return columnCount * characterWidth + leftInset + rightInset;
456 }
457
458 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
459 return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
460 }
461
462 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
463 return topInset + textNode.getLayoutBounds().getHeight() + bottomInset;
464 }
465
466 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
467 return getSkinnable().prefHeight(width);
468 }
469
470 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
471 return topInset + textNode.getBaselineOffset();
472 }
473
474 /**
475 * Updates the textTranslateX value for the Text node position. This is
476 * done for general layout, but care is taken to avoid resetting the
477 * position when there's a need to scroll the text due to caret movement,
478 * or when editing text that overflows on either side.
479 */
480 private void updateTextPos() {
481 double oldX = textTranslateX.get();
482 double newX;
483 double textNodeWidth = textNode.getLayoutBounds().getWidth();
484
485 switch (getHAlignment()) {
486 case CENTER:
487 double midPoint = textRight.get() / 2;
488 if (usePromptText.get()) {
489 // If a prompt is shown (which implies that the text is
490 // empty), then we align the Text node so that the caret will
491 // appear at the left of the centered prompt.
492 newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2;
493 promptNode.setLayoutX(newX);
494 } else {
495 newX = midPoint - textNodeWidth / 2;
496 }
497 // Update if there is space on the right
498 if (newX + textNodeWidth <= textRight.get()) {
499 textTranslateX.set(newX);
500 }
501 break;
502
503 case RIGHT:
504 newX = textRight.get() - textNodeWidth - caretWidth / 2;
505 // Update if there is space on the right
506 if (newX > oldX || newX > 0) {
507 textTranslateX.set(newX);
508 }
509 if (usePromptText.get()) {
510 promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() -
511 caretWidth / 2);
512 }
513 break;
514
515 case LEFT:
516 default:
517 newX = caretWidth / 2;
518 // Update if there is space on either side.
519 if (newX < oldX || newX + textNodeWidth <= textRight.get()) {
520 textTranslateX.set(newX);
521 }
522 if (usePromptText.get()) {
523 promptNode.layoutXProperty().set(newX);
524 }
525 }
526 }
527
528 // should be called when the padding changes, or the text box width, or
529 // the dot moves
530 protected void updateCaretOff() {
531 double delta = 0.0;
532 double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get();
533 // If the caret position is less than or equal to the left edge of the
534 // clip then the caret will be clipped. We want the caret to end up
535 // being positioned one pixel right of the clip's left edge. The same
536 // applies on the right edge (but going the other direction of course).
537 if (caretX < 0) {
538 // I'll end up with a negative number
539 delta = caretX;
540 } else if (caretX > (textRight.get() - caretWidth)) {
541 // I'll end up with a positive number
542 delta = caretX - (textRight.get() - caretWidth);
543 }
544
545 // If delta is negative, then translate in the negative direction
546 // to cause the text to scroll to the right. Vice-versa for positive.
547 switch (getHAlignment()) {
548 case CENTER:
549 textTranslateX.set(textTranslateX.get() - delta);
550 break;
551
552 case RIGHT:
553 textTranslateX.set(Math.max(textTranslateX.get() - delta,
554 textRight.get() - textNode.getLayoutBounds().getWidth() -
555 caretWidth / 2));
556 break;
557
558 case LEFT:
559 default:
560 textTranslateX.set(Math.min(textTranslateX.get() - delta,
561 caretWidth / 2));
562 }
563 if (SHOW_HANDLES) {
564 caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2 + 1);
565 }
566 }
567
568 /**
569 * Use this implementation instead of the one provided on TextInputControl.
570 * updateCaretOff would get called to position the caret, but the text needs
571 * to be scrolled appropriately.
572 */
573 public void replaceText(int start, int end, String txt) {
574 final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
575 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
576 getSkinnable().replaceText(start, end, txt);
577 scrollAfterDelete(textMaxXOld, caretMaxXOld);
578 }
579
580 /**
581 * Use this implementation instead of the one provided on TextInputControl
582 * Simply calls into TextInputControl.deletePrevious/NextChar and responds appropriately
583 * based on the return value.
584 */
585 public void deleteChar(boolean previous) {
586 final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
587 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
588 final boolean shouldBeep = previous ?
589 !getSkinnable().deletePreviousChar() :
590 !getSkinnable().deleteNextChar();
591
592 if (shouldBeep) {
593 // beep();
594 } else {
595 scrollAfterDelete(textMaxXOld, caretMaxXOld);
596 }
597 }
598
599 public void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) {
600 final Bounds textLayoutBounds = textNode.getLayoutBounds();
601 final Bounds textBounds = textNode.localToParent(textLayoutBounds);
602 final Bounds clipBounds = clip.getBoundsInParent();
603 final Bounds caretBounds = caretPath.getLayoutBounds();
604
605 switch (getHAlignment()) {
606 case RIGHT:
607 if (textBounds.getMaxX() > clipBounds.getMaxX()) {
608 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
609 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
610 if (textMaxXOld <= clipBounds.getMaxX()) {
611 delta = textMaxXOld - textBounds.getMaxX();
612 } else {
613 delta = clipBounds.getMaxX() - textBounds.getMaxX();
614 }
615 }
616 textTranslateX.set(textTranslateX.get() + delta);
617 } else {
618 updateTextPos();
619 }
620 break;
621
622 case LEFT:
623 case CENTER:
624 default:
625 if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 &&
626 textBounds.getMaxX() <= clipBounds.getMaxX()) {
627 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
628 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
629 if (textMaxXOld <= clipBounds.getMaxX()) {
630 delta = textMaxXOld - textBounds.getMaxX();
631 } else {
632 delta = clipBounds.getMaxX() - textBounds.getMaxX();
633 }
634 }
635 textTranslateX.set(textTranslateX.get() + delta);
636 }
637 }
638
639 updateCaretOff();
640 }
641
642 public HitInfo getIndex(double x, double y) {
643 // adjust the event to be in the same coordinate space as the
644 // text content of the textInputControl
645 Point2D p;
646
647 p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
648 y - snappedTopInset());
649 return textNode.impl_hitTestChar(translateCaretPosition(p));
650 }
651
652 public void positionCaret(HitInfo hit, boolean select) {
653 TextField textField = getSkinnable();
654 int pos = Utils.getHitInsertionIndex(hit, textField.textProperty().getValueSafe());
655
656 if (select) {
657 textField.selectPositionCaret(pos);
658 } else {
659 textField.positionCaret(pos);
660 }
661
662 setForwardBias(hit.isLeading());
663 }
664
665 @Override public Rectangle2D getCharacterBounds(int index) {
666 double x, y;
667 double width, height;
668 if (index == textNode.getText().length()) {
669 Bounds textNodeBounds = textNode.getBoundsInLocal();
670 x = textNodeBounds.getMaxX();
671 y = 0;
672 width = 0;
673 height = textNodeBounds.getMaxY();
674 } else {
675 characterBoundingPath.getElements().clear();
676 characterBoundingPath.getElements().addAll(textNode.impl_getRangeShape(index, index + 1));
677 characterBoundingPath.setLayoutX(textNode.getLayoutX());
678 characterBoundingPath.setLayoutY(textNode.getLayoutY());
679
680 Bounds bounds = characterBoundingPath.getBoundsInLocal();
681
682 x = bounds.getMinX();
683 y = bounds.getMinY();
684 // Sometimes the bounds is empty, in which case we must ignore the width/height
685 width = bounds.isEmpty() ? 0 : bounds.getWidth();
686 height = bounds.isEmpty() ? 0 : bounds.getHeight();
687 }
688
689 Bounds textBounds = textGroup.getBoundsInParent();
690
691 return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(),
692 y + textBounds.getMinY(), width, height);
693 }
694
695 @Override protected PathElement[] getUnderlineShape(int start, int end) {
696 return textNode.impl_getUnderlineShape(start, end);
697 }
698
699 @Override protected PathElement[] getRangeShape(int start, int end) {
700 return textNode.impl_getRangeShape(start, end);
701 }
702
703 @Override protected void addHighlight(List<? extends Node> nodes, int start) {
704 textGroup.getChildren().addAll(nodes);
705 }
706
707 @Override protected void removeHighlight(List<? extends Node> nodes) {
708 textGroup.getChildren().removeAll(nodes);
709 }
710
711 @Override public void nextCharacterVisually(boolean moveRight) {
712 if (isRTL()) {
713 // Text node is mirrored.
714 moveRight = !moveRight;
715 }
716
717 Bounds caretBounds = caretPath.getLayoutBounds();
718 if (caretPath.getElements().size() == 4) {
719 // The caret is split
720 // TODO: Find a better way to get the primary caret position
721 // instead of depending on the internal implementation.
722 // See RT-25465.
723 caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
724 }
725 double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
726 double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
727 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(hitX, hitY)));
728 Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
729 if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
730 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
731 hit.setLeading(!hit.isLeading());
732 }
733 positionCaret(hit, false);
734 }
735
736 @Override protected void layoutChildren(final double x, final double y,
737 final double w, final double h) {
738 super.layoutChildren(x, y, w, h);
739
740 if (textNode != null) {
741 double textY;
742 final Bounds textNodeBounds = textNode.getLayoutBounds();
743 final double ascent = textNode.getBaselineOffset();
744 final double descent = textNodeBounds.getHeight() - ascent;
745
746 switch (getSkinnable().getAlignment().getVpos()) {
747 case TOP:
748 textY = ascent;
749 break;
750
751 case CENTER:
752 textY = (ascent + textGroup.getHeight() - descent) / 2;
753 break;
754
755 case BOTTOM:
771 handleGroup.setLayoutX(x + textTranslateX.get());
772 handleGroup.setLayoutY(y);
773
774 // Resize handles for caret and anchor.
775 // IndexRange selection = textField.getSelection();
776 selectionHandle1.resize(selectionHandle1.prefWidth(-1),
777 selectionHandle1.prefHeight(-1));
778 selectionHandle2.resize(selectionHandle2.prefWidth(-1),
779 selectionHandle2.prefHeight(-1));
780 caretHandle.resize(caretHandle.prefWidth(-1),
781 caretHandle.prefHeight(-1));
782
783 Bounds b = caretPath.getBoundsInParent();
784 caretHandle.setLayoutY(b.getMaxY() - 1);
785 //selectionHandle1.setLayoutY(b.getMaxY() - 1);
786 selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
787 selectionHandle2.setLayoutY(b.getMaxY() - 1);
788 }
789 }
790
791 protected HPos getHAlignment() {
792 HPos hPos = getSkinnable().getAlignment().getHpos();
793 return hPos;
794 }
795
796 @Override public Point2D getMenuPosition() {
797 Point2D p = super.getMenuPosition();
798 if (p != null) {
799 p = new Point2D(Math.max(0, p.getX() - textNode.getLayoutX() - snappedLeftInset() + textTranslateX.get()),
800 Math.max(0, p.getY() - textNode.getLayoutY() - snappedTopInset()));
801 }
802 return p;
803 }
804
805 @Override protected String maskText(String txt) {
806 if (getSkinnable() instanceof PasswordField) {
807 int n = txt.length();
808 StringBuilder passwordBuilder = new StringBuilder(n);
809 for (int i = 0; i < n; i++) {
810 passwordBuilder.append(BULLET);
811 }
812
813 return passwordBuilder.toString();
814 } else {
815 return txt;
816 }
817 }
818
819 @Override
820 protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
821 switch (attribute) {
822 case BOUNDS_FOR_RANGE:
823 case OFFSET_AT_POINT:
824 return textNode.queryAccessibleAttribute(attribute, parameters);
825 default: return super.queryAccessibleAttribute(attribute, parameters);
826 }
827 }
828 }
|
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package javafx.scene.control.skin;
27
28 import com.sun.javafx.scene.control.behavior.BehaviorBase;
29 import com.sun.javafx.scene.control.behavior.TextAreaBehavior;
30 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
31 import com.sun.javafx.scene.control.skin.Utils;
32 import javafx.beans.binding.BooleanBinding;
33 import javafx.beans.binding.DoubleBinding;
34 import javafx.beans.binding.ObjectBinding;
35 import javafx.beans.binding.StringBinding;
36 import javafx.beans.property.DoubleProperty;
37 import javafx.beans.property.SimpleDoubleProperty;
38 import javafx.beans.value.ObservableBooleanValue;
39 import javafx.beans.value.ObservableDoubleValue;
40 import javafx.event.EventHandler;
41 import javafx.geometry.Bounds;
42 import javafx.geometry.HPos;
43 import javafx.geometry.Point2D;
44 import javafx.geometry.Rectangle2D;
45 import javafx.scene.AccessibleAttribute;
46 import javafx.scene.Group;
47 import javafx.scene.Node;
48 import javafx.scene.control.Accordion;
49 import javafx.scene.control.Button;
50 import javafx.scene.control.Control;
51 import javafx.scene.control.IndexRange;
52 import javafx.scene.control.PasswordField;
53 import javafx.scene.control.TextField;
54 import javafx.scene.input.MouseEvent;
55 import javafx.scene.layout.Pane;
56 import javafx.scene.paint.Color;
57 import javafx.scene.paint.Paint;
58 import javafx.scene.shape.Path;
59 import javafx.scene.shape.PathElement;
60 import javafx.scene.shape.Rectangle;
61 import javafx.scene.text.Text;
62 import java.util.List;
63 import com.sun.javafx.scene.control.behavior.TextFieldBehavior;
64 import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior;
65 import com.sun.javafx.scene.text.HitInfo;
66
67 /**
68 * Default skin implementation for the {@link TextField} control.
69 *
70 * @see TextField
71 * @since 9
72 */
73 public class TextFieldSkin extends TextInputControlSkin<TextField> {
74
75 /**************************************************************************
76 *
77 * Private fields
78 *
79 **************************************************************************/
80
81 private final TextFieldBehavior behavior;
82
83 /**
84 * This group contains the text, caret, and selection rectangle.
85 * It is clipped. The textNode, selectionHighlightPath, and
86 * caret are each translated individually when horizontal
87 * translation is needed to keep the caretPosition visible.
88 */
89 private Pane textGroup = new Pane();
90 private Group handleGroup;
91
92 /**
93 * The clip, applied to the textGroup. This makes sure that any
94 * text / selection wandering off the text box is clipped
95 */
96 private Rectangle clip = new Rectangle();
97 /**
98 * The node actually displaying the text. Note that it has the
99 * ability to render both the normal fill as well as the highlight
100 * fill, to perform hit testing, fetching of the selection
101 * highlight, and other such duties.
102 */
104 /**
105 *
106 * The node used for showing the prompt text.
107 */
108 private Text promptNode;
109 /**
110 * A path, provided by the textNode, which represents the area
111 * which is selected. The path elements which make up the
112 * selection must be updated whenever the selection changes. We
113 * don't need to keep track of text changes because those will
114 * force the selection to be updated.
115 */
116 private Path selectionHighlightPath = new Path();
117
118 private Path characterBoundingPath = new Path();
119 private ObservableBooleanValue usePromptText;
120 private DoubleProperty textTranslateX = new SimpleDoubleProperty(this, "textTranslateX");
121 private double caretWidth;
122
123 /**
124 * Right edge of the text region sans padding
125 */
126 private ObservableDoubleValue textRight;
127
128 private double pressX, pressY; // For dragging handles on embedded
129
130 // For use with PasswordField
131 static final char BULLET = '\u25cf';
132
133
134
135 /**************************************************************************
136 *
137 * Constructors
138 *
139 **************************************************************************/
140
141 /**
142 * Creates a new TextFieldSkin instance, installing the necessary child
143 * nodes into the Control {@link Control#getChildren() children} list, as
144 * well as the necessary input mappings for handling key, mouse, etc events.
145 *
146 * @param control The control that this skin should be installed onto.
147 */
148 public TextFieldSkin(final TextField control) {
149 super(control);
150
151 // install default input map for the text field control
152 this.behavior = (control instanceof PasswordField)
153 ? new PasswordFieldBehavior((PasswordField)control)
154 : new TextFieldBehavior(control);
155 this.behavior.setTextFieldSkin(this);
156 // control.setInputMap(behavior.getInputMap());
157
158 control.caretPositionProperty().addListener((observable, oldValue, newValue) -> {
159 if (control.getWidth() > 0) {
160 updateTextNodeCaretPos(control.getCaretPosition());
161 if (!isForwardBias()) {
162 setForwardBias(true);
163 }
164 updateCaretOff();
165 }
166 });
167
168 forwardBiasProperty().addListener(observable -> {
169 if (control.getWidth() > 0) {
170 updateTextNodeCaretPos(control.getCaretPosition());
171 updateCaretOff();
172 }
173 });
174
175 textRight = new DoubleBinding() {
176 { bind(textGroup.widthProperty()); }
177 @Override protected double computeValue() {
178 return textGroup.getWidth();
179 }
180 };
181
182 // Once this was crucial for performance, not sure now.
183 clip.setSmooth(false);
184 clip.setX(0);
185 clip.widthProperty().bind(textGroup.widthProperty());
186 clip.heightProperty().bind(textGroup.heightProperty());
187
188 // Add content
189 textGroup.setClip(clip);
190 // Hack to defeat the fact that otherwise when the caret blinks the parent group
191 // bounds are completely invalidated and therefore the dirty region is much
192 // larger than necessary.
193 textGroup.getChildren().addAll(selectionHighlightPath, textNode, new Group(caretPath));
194 getChildren().add(textGroup);
195 if (SHOW_HANDLES) {
196 handleGroup = new Group();
197 handleGroup.setManaged(false);
198 handleGroup.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2);
199 getChildren().add(handleGroup);
200 }
201
202 // Add text
203 textNode.setManaged(false);
204 textNode.getStyleClass().add("text");
205 textNode.fontProperty().bind(control.fontProperty());
206
207 textNode.layoutXProperty().bind(textTranslateX);
208 textNode.textProperty().bind(new StringBinding() {
209 { bind(control.textProperty()); }
210 @Override protected String computeValue() {
211 return maskText(control.textProperty().getValueSafe());
212 }
213 });
214 textNode.fillProperty().bind(textFillProperty());
215 textNode.impl_selectionFillProperty().bind(new ObjectBinding<Paint>() {
216 { bind(highlightTextFillProperty(), textFillProperty(), control.focusedProperty()); }
217 @Override protected Paint computeValue() {
218 return control.isFocused() ? highlightTextFillProperty().get() : textFillProperty().get();
219 }
220 });
221 // updated by listener on caretPosition to ensure order
222 updateTextNodeCaretPos(control.getCaretPosition());
223 control.selectionProperty().addListener(observable -> {
224 updateSelection();
225 });
226
227 // Add selection
228 selectionHighlightPath.setManaged(false);
229 selectionHighlightPath.setStroke(null);
230 selectionHighlightPath.layoutXProperty().bind(textTranslateX);
231 selectionHighlightPath.visibleProperty().bind(control.anchorProperty().isNotEqualTo(control.caretPositionProperty()).and(control.focusedProperty()));
232 selectionHighlightPath.fillProperty().bind(highlightFillProperty());
233 textNode.impl_selectionShapeProperty().addListener(observable -> {
234 updateSelection();
235 });
236
237 // Add caret
238 caretPath.setManaged(false);
239 caretPath.setStrokeWidth(1);
240 caretPath.fillProperty().bind(textFillProperty());
241 caretPath.strokeProperty().bind(textFillProperty());
242
243 // modifying visibility of the caret forces a layout-pass (RT-32373), so
244 // instead we modify the opacity.
245 caretPath.opacityProperty().bind(new DoubleBinding() {
246 { bind(caretVisibleProperty()); }
247 @Override protected double computeValue() {
248 return caretVisibleProperty().get() ? 1.0 : 0.0;
249 }
250 });
251 caretPath.layoutXProperty().bind(textTranslateX);
252 textNode.impl_caretShapeProperty().addListener(observable -> {
253 caretPath.getElements().setAll(textNode.impl_caretShapeProperty().get());
254 if (caretPath.getElements().size() == 0) {
255 // The caret pos is invalid.
256 updateTextNodeCaretPos(control.getCaretPosition());
257 } else if (caretPath.getElements().size() == 4) {
258 // The caret is split. Ignore and keep the previous width value.
259 } else {
260 caretWidth = Math.round(caretPath.getLayoutBounds().getWidth());
261 }
262 });
263
264 // Be sure to get the control to request layout when the font changes,
265 // since this will affect the pref height and pref width.
266 control.fontProperty().addListener(observable -> {
267 // I do both so that any cached values for prefWidth/height are cleared.
268 // The problem is that the skin is unmanaged and so calling request layout
269 // doesn't walk up the tree all the way. I think....
270 control.requestLayout();
271 getSkinnable().requestLayout();
272 });
273
274 registerChangeListener(control.prefColumnCountProperty(), e -> getSkinnable().requestLayout());
275 if (control.isFocused()) setCaretAnimating(true);
276
277 control.alignmentProperty().addListener(observable -> {
278 if (control.getWidth() > 0) {
279 updateTextPos();
280 updateCaretOff();
281 control.requestLayout();
282 }
283 });
284
285 usePromptText = new BooleanBinding() {
286 { bind(control.textProperty(),
287 control.promptTextProperty(),
288 promptTextFillProperty()); }
289 @Override protected boolean computeValue() {
290 String txt = control.getText();
291 String promptTxt = control.getPromptText();
292 return ((txt == null || txt.isEmpty()) &&
293 promptTxt != null && !promptTxt.isEmpty() &&
294 !getPromptTextFill().equals(Color.TRANSPARENT));
295 }
296 };
297
298 promptTextFillProperty().addListener(observable -> {
299 updateTextPos();
300 });
301
302 control.textProperty().addListener(observable -> {
303 if (!behavior.isEditing()) {
304 // Text changed, but not by user action
305 updateTextPos();
306 }
307 });
308
309 if (usePromptText.get()) {
310 createPromptNode();
311 }
312
313 usePromptText.addListener(observable -> {
314 createPromptNode();
315 control.requestLayout();
316 });
317
318 if (SHOW_HANDLES) {
319 selectionHandle1.setRotate(180);
320
321 EventHandler<MouseEvent> handlePressHandler = e -> {
322 pressX = e.getX();
323 pressY = e.getY();
324 e.consume();
325 };
326
327 caretHandle.setOnMousePressed(handlePressHandler);
328 selectionHandle1.setOnMousePressed(handlePressHandler);
329 selectionHandle2.setOnMousePressed(handlePressHandler);
330
331 caretHandle.setOnMouseDragged(e -> {
332 Point2D p = new Point2D(caretHandle.getLayoutX() + e.getX() + pressX - textNode.getLayoutX(),
333 caretHandle.getLayoutY() + e.getY() - pressY - 6);
334 HitInfo hit = textNode.impl_hitTestChar(p);
335 positionCaret(hit, false);
336 e.consume();
337 });
338
339 selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() {
340 @Override public void handle(MouseEvent e) {
341 TextField control = getSkinnable();
342 Point2D tp = textNode.localToScene(0, 0);
343 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2,
344 e.getSceneY() - tp.getY() - pressY - 6);
345 HitInfo hit = textNode.impl_hitTestChar(p);
346 int pos = hit.getCharIndex();
347 if (control.getAnchor() < control.getCaretPosition()) {
348 // Swap caret and anchor
349 control.selectRange(control.getCaretPosition(), control.getAnchor());
350 }
351 if (pos >= 0) {
352 if (pos >= control.getAnchor() - 1) {
353 hit.setCharIndex(Math.max(0, control.getAnchor() - 1));
354 }
355 positionCaret(hit, true);
356 }
357 e.consume();
358 }
359 });
360
361 selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() {
362 @Override public void handle(MouseEvent e) {
363 TextField control = getSkinnable();
364 Point2D tp = textNode.localToScene(0, 0);
365 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2,
366 e.getSceneY() - tp.getY() - pressY - 6);
367 HitInfo hit = textNode.impl_hitTestChar(p);
368 int pos = hit.getCharIndex();
369 if (control.getAnchor() > control.getCaretPosition()) {
370 // Swap caret and anchor
371 control.selectRange(control.getCaretPosition(), control.getAnchor());
372 }
373 if (pos > 0) {
374 if (pos <= control.getAnchor()) {
375 hit.setCharIndex(Math.min(control.getAnchor() + 1, control.getLength()));
376 }
377 positionCaret(hit, true);
378 }
379 e.consume();
380 }
381 });
382 }
383 }
384
385
386
387 /***************************************************************************
388 * *
389 * Public API *
390 * *
391 **************************************************************************/
392
393 /** {@inheritDoc} */
394 @Override public void dispose() {
395 super.dispose();
396
397 if (behavior != null) {
398 behavior.dispose();
399 }
400 }
401
402 /** {@inheritDoc} */
403 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
404 TextField textField = getSkinnable();
405
406 double characterWidth = fontMetrics.get().computeStringWidth("W");
407
408 int columnCount = textField.getPrefColumnCount();
409
410 return columnCount * characterWidth + leftInset + rightInset;
411 }
412
413 /** {@inheritDoc} */
414 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
415 return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
416 }
417
418 /** {@inheritDoc} */
419 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
420 return topInset + textNode.getLayoutBounds().getHeight() + bottomInset;
421 }
422
423 /** {@inheritDoc} */
424 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
425 return getSkinnable().prefHeight(width);
426 }
427
428 /** {@inheritDoc} */
429 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
430 return topInset + textNode.getBaselineOffset();
431 }
432
433 // Public for behavior
434 /**
435 * Replaces a range of characters with the given text.
436 *
437 * Call this implementation from behavior classes instead of the
438 * one provided on TextInputControl to ensure that the text
439 * scrolls as needed.
440 *
441 * @param start The starting index in the range, inclusive. This must be >= 0 and < the end.
442 * @param end The ending index in the range, exclusive. This is one-past the last character to
443 * delete (consistent with the String manipulation methods). This must be > the start,
444 * and <= the length of the text.
445 * @param text The text that is to replace the range. This must not be null.
446 * @see TextField#replaceText(int, int, String)
447 */
448 public void replaceText(int start, int end, String txt) {
449 final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
450 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
451 getSkinnable().replaceText(start, end, txt);
452 scrollAfterDelete(textMaxXOld, caretMaxXOld);
453 }
454
455 // Public for behavior
456 /**
457 * Deletes the character that follows or precedes the current
458 * caret position from the text if there is no selection, or
459 * deletes the selection if there is one.
460 *
461 * Call this implementation from behavior classes instead of the
462 * one provided on TextInputControl to ensure that the text
463 * scrolls as needed.
464 *
465 * @param previous whether to delete the preceding character.
466 */
467 public void deleteChar(boolean previous) {
468 final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
469 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
470 if (previous ? getSkinnable().deletePreviousChar() : getSkinnable().deleteNextChar()) {
471 scrollAfterDelete(textMaxXOld, caretMaxXOld);
472 }
473 }
474
475 // Public for behavior
476 /**
477 * Performs a hit test, mapping point to index in the content.
478 *
479 * @param x the x coordinate of the point.
480 * @param y the y coordinate of the point.
481 * @return a {@code TextPosInfo} object describing the index and forward bias.
482 */
483 public TextPosInfo getIndex(double x, double y) {
484 // adjust the event to be in the same coordinate space as the
485 // text content of the textInputControl
486 Point2D p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
487 y - snappedTopInset());
488 return new TextPosInfo(textNode.impl_hitTestChar(p));
489 }
490
491 // Public for behavior
492 /**
493 * Moves the caret to the specified position.
494 *
495 * @param hit the new position and forward bias of the caret.
496 * @param select whether to extend selection to the new position.
497 */
498 public void positionCaret(TextPosInfo hit, boolean select) {
499 TextField textField = getSkinnable();
500 int pos = Utils.getHitInsertionIndex(hit, textField.textProperty().getValueSafe());
501
502 if (select) {
503 textField.selectPositionCaret(pos);
504 } else {
505 textField.positionCaret(pos);
506 }
507
508 setForwardBias(hit.isLeading());
509 }
510
511 private void positionCaret(HitInfo hit, boolean select) {
512 positionCaret(new TextPosInfo(hit), select);
513 }
514
515 /** {@inheritDoc} */
516 @Override public Rectangle2D getCharacterBounds(int index) {
517 double x, y;
518 double width, height;
519 if (index == textNode.getText().length()) {
520 Bounds textNodeBounds = textNode.getBoundsInLocal();
521 x = textNodeBounds.getMaxX();
522 y = 0;
523 width = 0;
524 height = textNodeBounds.getMaxY();
525 } else {
526 characterBoundingPath.getElements().clear();
527 characterBoundingPath.getElements().addAll(textNode.impl_getRangeShape(index, index + 1));
528 characterBoundingPath.setLayoutX(textNode.getLayoutX());
529 characterBoundingPath.setLayoutY(textNode.getLayoutY());
530
531 Bounds bounds = characterBoundingPath.getBoundsInLocal();
532
533 x = bounds.getMinX();
534 y = bounds.getMinY();
535 // Sometimes the bounds is empty, in which case we must ignore the width/height
536 width = bounds.isEmpty() ? 0 : bounds.getWidth();
537 height = bounds.isEmpty() ? 0 : bounds.getHeight();
538 }
539
540 Bounds textBounds = textGroup.getBoundsInParent();
541
542 return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(),
543 y + textBounds.getMinY(), width, height);
544 }
545
546 /** {@inheritDoc} */
547 @Override protected PathElement[] getUnderlineShape(int start, int end) {
548 return textNode.impl_getUnderlineShape(start, end);
549 }
550
551 /** {@inheritDoc} */
552 @Override protected PathElement[] getRangeShape(int start, int end) {
553 return textNode.impl_getRangeShape(start, end);
554 }
555
556 /** {@inheritDoc} */
557 @Override protected void addHighlight(List<? extends Node> nodes, int start) {
558 textGroup.getChildren().addAll(nodes);
559 }
560
561 /** {@inheritDoc} */
562 @Override protected void removeHighlight(List<? extends Node> nodes) {
563 textGroup.getChildren().removeAll(nodes);
564 }
565
566 /** {@inheritDoc} */
567 @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) {
568 switch (unit) {
569 case CHARACTER:
570 switch (dir) {
571 case LEFT:
572 case RIGHT:
573 nextCharacterVisually(dir == Direction.RIGHT);
574 break;
575 default:
576 throw new IllegalArgumentException(""+dir);
577 }
578 break;
579 default:
580 throw new IllegalArgumentException(""+unit);
581 }
582 }
583
584 private void nextCharacterVisually(boolean moveRight) {
585 if (isRTL()) {
586 // Text node is mirrored.
587 moveRight = !moveRight;
588 }
589
590 Bounds caretBounds = caretPath.getLayoutBounds();
591 if (caretPath.getElements().size() == 4) {
592 // The caret is split
593 // TODO: Find a better way to get the primary caret position
594 // instead of depending on the internal implementation.
595 // See RT-25465.
596 caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
597 }
598 double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
599 double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
600 HitInfo hit = textNode.impl_hitTestChar(new Point2D(hitX, hitY));
601 Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
602 if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
603 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
604 hit.setLeading(!hit.isLeading());
605 }
606 positionCaret(hit, false);
607 }
608
609 /** {@inheritDoc} */
610 @Override protected void layoutChildren(final double x, final double y,
611 final double w, final double h) {
612 super.layoutChildren(x, y, w, h);
613
614 if (textNode != null) {
615 double textY;
616 final Bounds textNodeBounds = textNode.getLayoutBounds();
617 final double ascent = textNode.getBaselineOffset();
618 final double descent = textNodeBounds.getHeight() - ascent;
619
620 switch (getSkinnable().getAlignment().getVpos()) {
621 case TOP:
622 textY = ascent;
623 break;
624
625 case CENTER:
626 textY = (ascent + textGroup.getHeight() - descent) / 2;
627 break;
628
629 case BOTTOM:
645 handleGroup.setLayoutX(x + textTranslateX.get());
646 handleGroup.setLayoutY(y);
647
648 // Resize handles for caret and anchor.
649 // IndexRange selection = textField.getSelection();
650 selectionHandle1.resize(selectionHandle1.prefWidth(-1),
651 selectionHandle1.prefHeight(-1));
652 selectionHandle2.resize(selectionHandle2.prefWidth(-1),
653 selectionHandle2.prefHeight(-1));
654 caretHandle.resize(caretHandle.prefWidth(-1),
655 caretHandle.prefHeight(-1));
656
657 Bounds b = caretPath.getBoundsInParent();
658 caretHandle.setLayoutY(b.getMaxY() - 1);
659 //selectionHandle1.setLayoutY(b.getMaxY() - 1);
660 selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
661 selectionHandle2.setLayoutY(b.getMaxY() - 1);
662 }
663 }
664
665 private HPos getHAlignment() {
666 HPos hPos = getSkinnable().getAlignment().getHpos();
667 return hPos;
668 }
669
670 /** {@inheritDoc} */
671 @Override public Point2D getMenuPosition() {
672 Point2D p = super.getMenuPosition();
673 if (p != null) {
674 p = new Point2D(Math.max(0, p.getX() - textNode.getLayoutX() - snappedLeftInset() + textTranslateX.get()),
675 Math.max(0, p.getY() - textNode.getLayoutY() - snappedTopInset()));
676 }
677 return p;
678 }
679
680 /** {@inheritDoc} */
681 @Override protected String maskText(String txt) {
682 if (getSkinnable() instanceof PasswordField) {
683 int n = txt.length();
684 StringBuilder passwordBuilder = new StringBuilder(n);
685 for (int i = 0; i < n; i++) {
686 passwordBuilder.append(BULLET);
687 }
688
689 return passwordBuilder.toString();
690 } else {
691 return txt;
692 }
693 }
694
695 /** {@inheritDoc} */
696 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
697 switch (attribute) {
698 case BOUNDS_FOR_RANGE:
699 case OFFSET_AT_POINT:
700 return textNode.queryAccessibleAttribute(attribute, parameters);
701 default: return super.queryAccessibleAttribute(attribute, parameters);
702 }
703 }
704
705
706
707 /**************************************************************************
708 *
709 * Private implementation
710 *
711 **************************************************************************/
712
713 TextInputControlBehavior getBehavior() {
714 return behavior;
715 }
716
717 private void updateTextNodeCaretPos(int pos) {
718 if (pos == 0 || isForwardBias()) {
719 textNode.setImpl_caretPosition(pos);
720 } else {
721 textNode.setImpl_caretPosition(pos - 1);
722 }
723 textNode.impl_caretBiasProperty().set(isForwardBias());
724 }
725
726 private void createPromptNode() {
727 if (promptNode != null || !usePromptText.get()) return;
728
729 promptNode = new Text();
730 textGroup.getChildren().add(0, promptNode);
731 promptNode.setManaged(false);
732 promptNode.getStyleClass().add("text");
733 promptNode.visibleProperty().bind(usePromptText);
734 promptNode.fontProperty().bind(getSkinnable().fontProperty());
735
736 promptNode.textProperty().bind(getSkinnable().promptTextProperty());
737 promptNode.fillProperty().bind(promptTextFillProperty());
738 updateSelection();
739 }
740
741 private void updateSelection() {
742 TextField textField = getSkinnable();
743 IndexRange newValue = textField.getSelection();
744
745 if (newValue == null || newValue.getLength() == 0) {
746 textNode.impl_selectionStartProperty().set(-1);
747 textNode.impl_selectionEndProperty().set(-1);
748 } else {
749 textNode.impl_selectionStartProperty().set(newValue.getStart());
750 // This intermediate value is needed to force selection shape layout.
751 textNode.impl_selectionEndProperty().set(newValue.getStart());
752 textNode.impl_selectionEndProperty().set(newValue.getEnd());
753 }
754
755 PathElement[] elements = textNode.impl_selectionShapeProperty().get();
756 if (elements == null) {
757 selectionHighlightPath.getElements().clear();
758 } else {
759 selectionHighlightPath.getElements().setAll(elements);
760 }
761
762 if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) {
763 int caretPos = textField.getCaretPosition();
764 int anchorPos = textField.getAnchor();
765
766 {
767 // Position the handle for the anchor. This could be handle1 or handle2.
768 // Do this before positioning the handle for the caret.
769 updateTextNodeCaretPos(anchorPos);
770 Bounds b = caretPath.getBoundsInParent();
771 if (caretPos < anchorPos) {
772 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
773 } else {
774 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
775 }
776 }
777
778 {
779 // Position handle for the caret. This could be handle1 or handle2.
780 updateTextNodeCaretPos(caretPos);
781 Bounds b = caretPath.getBoundsInParent();
782 if (caretPos < anchorPos) {
783 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
784 } else {
785 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
786 }
787 }
788 }
789 }
790
791 /**
792 * Updates the textTranslateX value for the Text node position. This is
793 * done for general layout, but care is taken to avoid resetting the
794 * position when there's a need to scroll the text due to caret movement,
795 * or when editing text that overflows on either side.
796 */
797 private void updateTextPos() {
798 double oldX = textTranslateX.get();
799 double newX;
800 double textNodeWidth = textNode.getLayoutBounds().getWidth();
801
802 switch (getHAlignment()) {
803 case CENTER:
804 double midPoint = textRight.get() / 2;
805 if (usePromptText.get()) {
806 // If a prompt is shown (which implies that the text is
807 // empty), then we align the Text node so that the caret will
808 // appear at the left of the centered prompt.
809 newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2;
810 promptNode.setLayoutX(newX);
811 } else {
812 newX = midPoint - textNodeWidth / 2;
813 }
814 // Update if there is space on the right
815 if (newX + textNodeWidth <= textRight.get()) {
816 textTranslateX.set(newX);
817 }
818 break;
819
820 case RIGHT:
821 newX = textRight.get() - textNodeWidth - caretWidth / 2;
822 // Update if there is space on the right
823 if (newX > oldX || newX > 0) {
824 textTranslateX.set(newX);
825 }
826 if (usePromptText.get()) {
827 promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() -
828 caretWidth / 2);
829 }
830 break;
831
832 case LEFT:
833 default:
834 newX = caretWidth / 2;
835 // Update if there is space on either side.
836 if (newX < oldX || newX + textNodeWidth <= textRight.get()) {
837 textTranslateX.set(newX);
838 }
839 if (usePromptText.get()) {
840 promptNode.layoutXProperty().set(newX);
841 }
842 }
843 }
844
845 // should be called when the padding changes, or the text box width, or
846 // the dot moves
847 private void updateCaretOff() {
848 double delta = 0.0;
849 double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get();
850 // If the caret position is less than or equal to the left edge of the
851 // clip then the caret will be clipped. We want the caret to end up
852 // being positioned one pixel right of the clip's left edge. The same
853 // applies on the right edge (but going the other direction of course).
854 if (caretX < 0) {
855 // I'll end up with a negative number
856 delta = caretX;
857 } else if (caretX > (textRight.get() - caretWidth)) {
858 // I'll end up with a positive number
859 delta = caretX - (textRight.get() - caretWidth);
860 }
861
862 // If delta is negative, then translate in the negative direction
863 // to cause the text to scroll to the right. Vice-versa for positive.
864 switch (getHAlignment()) {
865 case CENTER:
866 textTranslateX.set(textTranslateX.get() - delta);
867 break;
868
869 case RIGHT:
870 textTranslateX.set(Math.max(textTranslateX.get() - delta,
871 textRight.get() - textNode.getLayoutBounds().getWidth() -
872 caretWidth / 2));
873 break;
874
875 case LEFT:
876 default:
877 textTranslateX.set(Math.min(textTranslateX.get() - delta,
878 caretWidth / 2));
879 }
880 if (SHOW_HANDLES) {
881 caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2 + 1);
882 }
883 }
884
885 private void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) {
886 final Bounds textLayoutBounds = textNode.getLayoutBounds();
887 final Bounds textBounds = textNode.localToParent(textLayoutBounds);
888 final Bounds clipBounds = clip.getBoundsInParent();
889 final Bounds caretBounds = caretPath.getLayoutBounds();
890
891 switch (getHAlignment()) {
892 case RIGHT:
893 if (textBounds.getMaxX() > clipBounds.getMaxX()) {
894 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
895 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
896 if (textMaxXOld <= clipBounds.getMaxX()) {
897 delta = textMaxXOld - textBounds.getMaxX();
898 } else {
899 delta = clipBounds.getMaxX() - textBounds.getMaxX();
900 }
901 }
902 textTranslateX.set(textTranslateX.get() + delta);
903 } else {
904 updateTextPos();
905 }
906 break;
907
908 case LEFT:
909 case CENTER:
910 default:
911 if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 &&
912 textBounds.getMaxX() <= clipBounds.getMaxX()) {
913 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
914 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
915 if (textMaxXOld <= clipBounds.getMaxX()) {
916 delta = textMaxXOld - textBounds.getMaxX();
917 } else {
918 delta = clipBounds.getMaxX() - textBounds.getMaxX();
919 }
920 }
921 textTranslateX.set(textTranslateX.get() + delta);
922 }
923 }
924
925 updateCaretOff();
926 }
927 }
|