1 /*
2 * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
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 com.sun.javafx.scene.input.ExtendedInputMethodRequests;
29 import javafx.animation.KeyFrame;
30 import javafx.animation.Timeline;
31 import javafx.application.ConditionalFeature;
32 import javafx.application.Platform;
33 import javafx.beans.binding.BooleanBinding;
34 import javafx.beans.binding.ObjectBinding;
35 import javafx.beans.property.BooleanProperty;
36 import javafx.beans.property.ObjectProperty;
37 import javafx.beans.property.SimpleBooleanProperty;
38 import javafx.beans.value.ObservableBooleanValue;
39 import javafx.beans.value.ObservableObjectValue;
40 import javafx.collections.ObservableList;
41 import javafx.css.CssMetaData;
42 import javafx.css.Styleable;
43 import javafx.css.StyleableBooleanProperty;
44 import javafx.css.StyleableObjectProperty;
45 import javafx.css.StyleableProperty;
46 import javafx.geometry.NodeOrientation;
47 import javafx.geometry.Point2D;
48 import javafx.geometry.Rectangle2D;
49 import javafx.scene.AccessibleAction;
50 import javafx.scene.Node;
51 import javafx.scene.Scene;
52 import javafx.scene.control.ContextMenu;
53 import javafx.scene.control.IndexRange;
54 import javafx.scene.control.MenuItem;
55 import javafx.scene.control.SeparatorMenuItem;
56 import javafx.scene.control.SkinBase;
57 import javafx.scene.control.TextInputControl;
58 import javafx.scene.input.Clipboard;
59 import javafx.scene.input.InputMethodEvent;
60 import javafx.scene.input.InputMethodHighlight;
61 import javafx.scene.input.InputMethodTextRun;
62 import javafx.scene.layout.StackPane;
63 import javafx.scene.paint.Color;
64 import javafx.scene.paint.Paint;
65 import javafx.scene.shape.ClosePath;
66 import javafx.scene.shape.HLineTo;
67 import javafx.scene.shape.Line;
68 import javafx.scene.shape.LineTo;
69 import javafx.scene.shape.MoveTo;
70 import javafx.scene.shape.Path;
71 import javafx.scene.shape.PathElement;
72 import javafx.scene.shape.Shape;
73 import javafx.scene.shape.VLineTo;
74 import javafx.stage.Window;
75 import javafx.util.Duration;
76 import java.lang.ref.WeakReference;
77 import java.util.ArrayList;
78 import java.util.Collections;
79 import java.util.List;
80 import com.sun.javafx.PlatformUtil;
81 import com.sun.javafx.css.converters.BooleanConverter;
82 import com.sun.javafx.css.converters.PaintConverter;
83 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
84 import com.sun.javafx.tk.FontMetrics;
85 import com.sun.javafx.tk.Toolkit;
86 import static com.sun.javafx.PlatformUtil.isWindows;
87 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
88 import java.security.AccessController;
89 import java.security.PrivilegedAction;
90
91 /**
92 * Abstract base class for text input skins.
93 */
94 public abstract class TextInputControlSkin<T extends TextInputControl, B extends TextInputControlBehavior<T>> extends BehaviorSkinBase<T, B> {
95
96 static boolean preload = false;
97 static {
98 AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
99 String s = System.getProperty("com.sun.javafx.virtualKeyboard.preload");
100 if (s != null) {
101 if (s.equalsIgnoreCase("PRERENDER")) {
102 preload = true;
103 }
104 }
105 return null;
106 });
107 }
108
109 /**
110 * Specifies whether we ought to show handles. We should do it on touch platforms, but not
111 * iOS (and maybe not Android either?)
112 */
113 protected static final boolean SHOW_HANDLES = IS_TOUCH_SUPPORTED && !PlatformUtil.isIOS();
114
115 protected final ObservableObjectValue<FontMetrics> fontMetrics;
116
117 /**
118 * The fill to use for the text under normal conditions
119 */
120 protected final ObjectProperty<Paint> textFill = new StyleableObjectProperty<Paint>(Color.BLACK) {
121 @Override public Object getBean() {
122 return TextInputControlSkin.this;
123 }
124
125 @Override public String getName() {
126 return "textFill";
127 }
128
129 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
130 return StyleableProperties.TEXT_FILL;
131 }
132 };
133
134 protected final ObjectProperty<Paint> promptTextFill = new StyleableObjectProperty<Paint>(Color.GRAY) {
135 @Override public Object getBean() {
136 return TextInputControlSkin.this;
137 }
138
139 @Override public String getName() {
140 return "promptTextFill";
141 }
142
143 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
144 return StyleableProperties.PROMPT_TEXT_FILL;
145 }
146 };
147
148 /**
149 * The fill to use for the text when highlighted.
150 */
151 protected final ObjectProperty<Paint> highlightFill = new StyleableObjectProperty<Paint>(Color.DODGERBLUE) {
152 @Override protected void invalidated() {
153 updateHighlightFill();
154 }
155
156 @Override public Object getBean() {
157 return TextInputControlSkin.this;
158 }
159
160 @Override public String getName() {
161 return "highlightFill";
162 }
163
164 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
165 return StyleableProperties.HIGHLIGHT_FILL;
166 }
167 };
168
169 protected final ObjectProperty<Paint> highlightTextFill = new StyleableObjectProperty<Paint>(Color.WHITE) {
170 @Override protected void invalidated() {
171 updateHighlightTextFill();
172 }
173
174 @Override public Object getBean() {
175 return TextInputControlSkin.this;
176 }
177
178 @Override public String getName() {
179 return "highlightTextFill";
180 }
181
182 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
183 return StyleableProperties.HIGHLIGHT_TEXT_FILL;
184 }
185 };
186
187 protected final BooleanProperty displayCaret = new StyleableBooleanProperty(true) {
188 @Override public Object getBean() {
189 return TextInputControlSkin.this;
190 }
191
192 @Override public String getName() {
193 return "displayCaret";
194 }
195
196 @Override public CssMetaData<TextInputControl,Boolean> getCssMetaData() {
197 return StyleableProperties.DISPLAY_CARET;
198 }
199 };
200
201 private BooleanProperty forwardBias = new SimpleBooleanProperty(this, "forwardBias", true);
202 public BooleanProperty forwardBiasProperty() {
203 return forwardBias;
204 }
205 public void setForwardBias(boolean isLeading) {
206 forwardBias.set(isLeading);
207 }
208 public boolean isForwardBias() {
209 return forwardBias.get();
210 }
211
212 private BooleanProperty blink = new SimpleBooleanProperty(this, "blink", true);
213 protected ObservableBooleanValue caretVisible;
214 private CaretBlinking caretBlinking = new CaretBlinking(blink);
215
216 /**
217 * A path, provided by the textNode, which represents the caret.
218 * I assume this has to be updated whenever the caretPosition
219 * changes. Perhaps more frequently (including text changes),
220 * but I'm not sure.
221 */
222 protected final Path caretPath = new Path();
223
224 protected StackPane caretHandle = null;
225 protected StackPane selectionHandle1 = null;
226 protected StackPane selectionHandle2 = null;
227
228 public Point2D getMenuPosition() {
229 if (SHOW_HANDLES) {
230 if (caretHandle.isVisible()) {
231 return new Point2D(caretHandle.getLayoutX() + caretHandle.getWidth() / 2,
232 caretHandle.getLayoutY());
233 } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
234 return new Point2D((selectionHandle1.getLayoutX() + selectionHandle1.getWidth() / 2 +
235 selectionHandle2.getLayoutX() + selectionHandle2.getWidth() / 2) / 2,
236 selectionHandle2.getLayoutY() + selectionHandle2.getHeight() / 2);
237 } else {
238 return null;
239 }
240 } else {
241 throw new UnsupportedOperationException();
242 }
243 }
244
245
246 private final static boolean IS_FXVK_SUPPORTED = Platform.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD);
247 private static boolean USE_FXVK = IS_FXVK_SUPPORTED;
248
249 /* For testing only */
250 static int vkType = -1;
251 public void toggleUseVK() {
252 vkType++;
253 if (vkType < 4) {
254 USE_FXVK = true;
255 getSkinnable().getProperties().put(FXVK.VK_TYPE_PROP_KEY, FXVK.VK_TYPE_NAMES[vkType]);
256 FXVK.attach(getSkinnable());
257 } else {
258 FXVK.detach();
259 vkType = -1;
260 USE_FXVK = false;
261 }
262 }
263
264
265 public TextInputControlSkin(final T textInput, final B behavior) {
266 super(textInput, behavior);
267
268 fontMetrics = new ObjectBinding<FontMetrics>() {
269 { bind(textInput.fontProperty()); }
270 @Override protected FontMetrics computeValue() {
271 invalidateMetrics();
272 return Toolkit.getToolkit().getFontLoader().getFontMetrics(textInput.getFont());
273 }
274 };
275
276 /**
277 * The caret is visible when the text box is focused AND when the selection
278 * is empty. If the selection is non empty or the text box is not focused
279 * then we don't want to show the caret. Also, we show the caret while
280 * performing some operations such as most key strokes. In that case we
281 * simply toggle its opacity.
282 * <p>
283 */
284 caretVisible = new BooleanBinding() {
285 { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
286 textInput.disabledProperty(), textInput.editableProperty(), displayCaret, blink);}
287 @Override protected boolean computeValue() {
288 // RT-10682: On Windows, we show the caret during selection, but on others we hide it
289 return !blink.get() && displayCaret.get() && textInput.isFocused() &&
290 (isWindows() || (textInput.getCaretPosition() == textInput.getAnchor())) &&
291 !textInput.isDisabled() &&
292 textInput.isEditable();
293 }
294 };
295
296 if (SHOW_HANDLES) {
297 caretHandle = new StackPane();
298 selectionHandle1 = new StackPane();
299 selectionHandle2 = new StackPane();
300
301 caretHandle.setManaged(false);
302 selectionHandle1.setManaged(false);
303 selectionHandle2.setManaged(false);
304
305 caretHandle.visibleProperty().bind(new BooleanBinding() {
306 { bind(textInput.focusedProperty(), textInput.anchorProperty(),
307 textInput.caretPositionProperty(), textInput.disabledProperty(),
308 textInput.editableProperty(), textInput.lengthProperty(), displayCaret);}
309 @Override protected boolean computeValue() {
310 return (displayCaret.get() && textInput.isFocused() &&
311 textInput.getCaretPosition() == textInput.getAnchor() &&
312 !textInput.isDisabled() && textInput.isEditable() &&
313 textInput.getLength() > 0);
314 }
315 });
316
317
318 selectionHandle1.visibleProperty().bind(new BooleanBinding() {
319 { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
320 textInput.disabledProperty(), displayCaret);}
321 @Override protected boolean computeValue() {
322 return (displayCaret.get() && textInput.isFocused() &&
323 textInput.getCaretPosition() != textInput.getAnchor() &&
324 !textInput.isDisabled());
325 }
326 });
327
328
329 selectionHandle2.visibleProperty().bind(new BooleanBinding() {
330 { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
331 textInput.disabledProperty(), displayCaret);}
332 @Override protected boolean computeValue() {
333 return (displayCaret.get() && textInput.isFocused() &&
334 textInput.getCaretPosition() != textInput.getAnchor() &&
335 !textInput.isDisabled());
336 }
337 });
338
339
340 caretHandle.getStyleClass().setAll("caret-handle");
341 selectionHandle1.getStyleClass().setAll("selection-handle");
342 selectionHandle2.getStyleClass().setAll("selection-handle");
343
344 selectionHandle1.setId("selection-handle-1");
345 selectionHandle2.setId("selection-handle-2");
346 }
347
348 if (IS_FXVK_SUPPORTED) {
349 if (preload) {
350 Scene scene = textInput.getScene();
351 if (scene != null) {
352 Window window = scene.getWindow();
353 if (window != null) {
354 FXVK.init(textInput);
355 }
356 }
357 }
358 textInput.focusedProperty().addListener(observable -> {
359 if (USE_FXVK) {
360 Scene scene = getSkinnable().getScene();
361 if (textInput.isEditable() && textInput.isFocused()) {
362 FXVK.attach(textInput);
363 } else if (scene == null ||
364 scene.getWindow() == null ||
365 !scene.getWindow().isFocused() ||
366 !(scene.getFocusOwner() instanceof TextInputControl &&
367 ((TextInputControl)scene.getFocusOwner()).isEditable())) {
368 FXVK.detach();
369 }
370 }
371 });
372 }
373
374 if (textInput.getOnInputMethodTextChanged() == null) {
375 textInput.setOnInputMethodTextChanged(event -> {
376 handleInputMethodEvent(event);
377 });
378 }
379
380 textInput.setInputMethodRequests(new ExtendedInputMethodRequests() {
381 @Override public Point2D getTextLocation(int offset) {
382 Scene scene = getSkinnable().getScene();
383 Window window = scene.getWindow();
384 // Don't use imstart here because it isn't initialized yet.
385 Rectangle2D characterBounds = getCharacterBounds(textInput.getSelection().getStart() + offset);
386 Point2D p = getSkinnable().localToScene(characterBounds.getMinX(), characterBounds.getMaxY());
387 Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(),
388 window.getY() + scene.getY() + p.getY());
389 return location;
390 }
391
392 @Override
393 public int getLocationOffset(int x, int y) {
394 return getInsertionPoint(x, y);
395 }
396
397 @Override
398 public void cancelLatestCommittedText() {
399 // TODO
400 }
401
402 @Override
403 public String getSelectedText() {
404 TextInputControl textInput = getSkinnable();
405 IndexRange selection = textInput.getSelection();
406
407 return textInput.getText(selection.getStart(), selection.getEnd());
408 }
409
410 @Override
411 public int getInsertPositionOffset() {
412 int caretPosition = getSkinnable().getCaretPosition();
413 if (caretPosition < imstart) {
414 return caretPosition;
415 } else if (caretPosition < imstart + imlength) {
416 return imstart;
417 } else {
418 return caretPosition - imlength;
419 }
420 }
421
422 @Override
423 public String getCommittedText(int begin, int end) {
424 TextInputControl textInput = getSkinnable();
425 if (begin < imstart) {
426 if (end <= imstart) {
427 return textInput.getText(begin, end);
428 } else {
429 return textInput.getText(begin, imstart) + textInput.getText(imstart + imlength, end + imlength);
430 }
431 } else {
432 return textInput.getText(begin + imlength, end + imlength);
433 }
434 }
435
436 @Override
437 public int getCommittedTextLength() {
438 return getSkinnable().getText().length() - imlength;
439 }
440 });
441 }
442
443 // For use with PasswordField in TextFieldSkin
444 protected String maskText(String txt) {
445 return txt;
446 }
447
448
449 /**
450 * Returns the character at a given offset.
451 *
452 * @param index
453 */
454 public char getCharacter(int index) { return '\0'; }
455
456 /**
457 * Returns the insertion point for a given location.
458 *
459 * @param x
460 * @param y
461 */
462 public int getInsertionPoint(double x, double y) { return 0; }
463
464 /**
465 * Returns the bounds of the character at a given index.
466 *
467 * @param index
468 */
469 public Rectangle2D getCharacterBounds(int index) { return null; }
470
471 /**
472 * Ensures that the character at a given index is visible.
473 *
474 * @param index
475 */
476 public void scrollCharacterToVisible(int index) {}
477
478 protected void invalidateMetrics() {
479 }
480
481 protected void updateTextFill() {};
482 protected void updateHighlightFill() {};
483 protected void updateHighlightTextFill() {};
484
485 // Start/Length of the text under input method composition
486 private int imstart;
487 private int imlength;
488 // Holds concrete attributes for the composition runs
489 private List<Shape> imattrs = new java.util.ArrayList<Shape>();
490
491 protected void handleInputMethodEvent(InputMethodEvent event) {
492 final TextInputControl textInput = getSkinnable();
493 if (textInput.isEditable() && !textInput.textProperty().isBound() && !textInput.isDisabled()) {
494
495 // just replace the text on iOS
496 if (PlatformUtil.isIOS()) {
497 textInput.setText(event.getCommitted());
498 return;
499 }
500
501 // remove previous input method text (if any) or selected text
502 if (imlength != 0) {
503 removeHighlight(imattrs);
504 imattrs.clear();
505 textInput.selectRange(imstart, imstart + imlength);
506 }
507
508 // Insert committed text
509 if (event.getCommitted().length() != 0) {
520 textInput.replaceText(textInput.getSelection(), composed.toString());
521 imlength = composed.length();
522 if (imlength != 0) {
523 int pos = imstart;
524 for (InputMethodTextRun run : event.getComposed()) {
525 int endPos = pos + run.getText().length();
526 createInputMethodAttributes(run.getHighlight(), pos, endPos);
527 pos = endPos;
528 }
529 addHighlight(imattrs, imstart);
530
531 // Set caret position in composed text
532 int caretPos = event.getCaretPosition();
533 if (caretPos >= 0 && caretPos < imlength) {
534 textInput.selectRange(imstart + caretPos, imstart + caretPos);
535 }
536 }
537 }
538 }
539
540 protected abstract PathElement[] getUnderlineShape(int start, int end);
541 protected abstract PathElement[] getRangeShape(int start, int end);
542 protected abstract void addHighlight(List<? extends Node> nodes, int start);
543 protected abstract void removeHighlight(List<? extends Node> nodes);
544 public abstract void nextCharacterVisually(boolean moveRight);
545
546 private void createInputMethodAttributes(InputMethodHighlight highlight, int start, int end) {
547 double minX = 0f;
548 double maxX = 0f;
549 double minY = 0f;
550 double maxY = 0f;
551
552 PathElement elements[] = getUnderlineShape(start, end);
553 for (int i = 0; i < elements.length; i++) {
554 PathElement pe = elements[i];
555 if (pe instanceof MoveTo) {
556 minX = maxX = ((MoveTo)pe).getX();
557 minY = maxY = ((MoveTo)pe).getY();
558 } else if (pe instanceof LineTo) {
559 minX = (minX < ((LineTo)pe).getX() ? minX : ((LineTo)pe).getX());
560 maxX = (maxX > ((LineTo)pe).getX() ? maxX : ((LineTo)pe).getX());
561 minY = (minY < ((LineTo)pe).getY() ? minY : ((LineTo)pe).getY());
562 maxY = (maxY > ((LineTo)pe).getY() ? maxY : ((LineTo)pe).getY());
563 } else if (pe instanceof HLineTo) {
564 minX = (minX < ((HLineTo)pe).getX() ? minX : ((HLineTo)pe).getX());
590 } else if (highlight == InputMethodHighlight.SELECTED_CONVERTED) {
591 // thick underline.
592 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
593 attr.setStroke(textFill.get());
594 attr.setStrokeWidth((maxY - minY) * 3);
595 } else if (highlight == InputMethodHighlight.UNSELECTED_CONVERTED) {
596 // single underline.
597 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
598 attr.setStroke(textFill.get());
599 attr.setStrokeWidth(maxY - minY);
600 }
601
602 if (attr != null) {
603 attr.setManaged(false);
604 imattrs.add(attr);
605 }
606 }
607 }
608 }
609
610 protected boolean isRTL() {
611 return (getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT);
612 };
613
614 public void setCaretAnimating(boolean value) {
615 if (value) {
616 caretBlinking.start();
617 } else {
618 caretBlinking.stop();
619 blink.set(true);
620 }
621 }
622
623 private static final class CaretBlinking {
624 private final Timeline caretTimeline;
625 private final WeakReference<BooleanProperty> blinkPropertyRef;
626
627 public CaretBlinking(final BooleanProperty blinkProperty) {
628 blinkPropertyRef =
629 new WeakReference<BooleanProperty>(blinkProperty);
630
631 caretTimeline = new Timeline();
632 caretTimeline.setCycleCount(Timeline.INDEFINITE);
633 caretTimeline.getKeyFrames().addAll(
634 new KeyFrame(Duration.ZERO,
635 event -> {
636 setBlink(false);
637 }
638 ),
639 new KeyFrame(Duration.seconds(.5),
640 event -> {
641 setBlink(true);
642 }
643 ),
644 new KeyFrame(Duration.seconds(1)));
645 }
646
647 public void start() {
648 caretTimeline.play();
649 }
650
651 public void stop() {
652 caretTimeline.stop();
653 }
654
655 private void setBlink(final boolean value) {
656 final BooleanProperty blinkProperty = blinkPropertyRef.get();
657 if (blinkProperty == null) {
658 caretTimeline.stop();
659 return;
660 }
661
662 blinkProperty.set(value);
663 }
664 }
665
666 class ContextMenuItem extends MenuItem {
667 ContextMenuItem(final String action) {
668 super(getString("TextInputControl.menu." + action));
669 setOnAction(e -> {
670 getBehavior().callAction(action);
671 });
672 }
673 }
674
675 final MenuItem undoMI = new ContextMenuItem("Undo");
676 final MenuItem redoMI = new ContextMenuItem("Redo");
677 final MenuItem cutMI = new ContextMenuItem("Cut");
678 final MenuItem copyMI = new ContextMenuItem("Copy");
679 final MenuItem pasteMI = new ContextMenuItem("Paste");
680 final MenuItem deleteMI = new ContextMenuItem("DeleteSelection");
681 final MenuItem selectWordMI = new ContextMenuItem("SelectWord");
682 final MenuItem selectAllMI = new ContextMenuItem("SelectAll");
683 final MenuItem separatorMI = new SeparatorMenuItem();
684
685 public void populateContextMenu(ContextMenu contextMenu) {
686 TextInputControl textInputControl = getSkinnable();
687 boolean editable = textInputControl.isEditable();
688 boolean hasText = (textInputControl.getLength() > 0);
689 boolean hasSelection = (textInputControl.getSelection().getLength() > 0);
690 boolean maskText = (maskText("A") != "A");
691 ObservableList<MenuItem> items = contextMenu.getItems();
692
693 if (SHOW_HANDLES) {
694 items.clear();
695 if (!maskText && hasSelection) {
696 if (editable) {
697 items.add(cutMI);
698 }
699 items.add(copyMI);
700 }
701 if (editable && Clipboard.getSystemClipboard().hasString()) {
702 items.add(pasteMI);
703 }
704 if (hasText) {
705 if (!hasSelection) {
706 items.add(selectWordMI);
707 }
708 items.add(selectAllMI);
709 }
710 selectWordMI.getProperties().put("refreshMenu", Boolean.TRUE);
711 selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
712 } else {
713 if (editable) {
714 items.setAll(undoMI, redoMI, cutMI, copyMI, pasteMI, deleteMI,
715 separatorMI, selectAllMI);
716 } else {
717 items.setAll(copyMI, separatorMI, selectAllMI);
718 }
719 undoMI.setDisable(!getSkinnable().isUndoable());
720 redoMI.setDisable(!getSkinnable().isRedoable());
721 cutMI.setDisable(maskText || !hasSelection);
722 copyMI.setDisable(maskText || !hasSelection);
723 pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString());
724 deleteMI.setDisable(!hasSelection);
725 }
726 }
727
728 private static class StyleableProperties {
729 private static final CssMetaData<TextInputControl,Paint> TEXT_FILL =
730 new CssMetaData<TextInputControl,Paint>("-fx-text-fill",
731 PaintConverter.getInstance(), Color.BLACK) {
732
733 @Override
734 public boolean isSettable(TextInputControl n) {
735 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
736 return skin.textFill == null || !skin.textFill.isBound();
737 }
738
739 @Override @SuppressWarnings("unchecked")
740 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
741 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
742 return (StyleableProperty<Paint>)skin.textFill;
743 }
744 };
745
746 private static final CssMetaData<TextInputControl,Paint> PROMPT_TEXT_FILL =
747 new CssMetaData<TextInputControl,Paint>("-fx-prompt-text-fill",
748 PaintConverter.getInstance(), Color.GRAY) {
749
750 @Override
751 public boolean isSettable(TextInputControl n) {
752 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
753 return skin.promptTextFill == null || !skin.promptTextFill.isBound();
754 }
755
756 @Override @SuppressWarnings("unchecked")
757 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
758 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
759 return (StyleableProperty<Paint>)skin.promptTextFill;
760 }
761 };
762
763 private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_FILL =
764 new CssMetaData<TextInputControl,Paint>("-fx-highlight-fill",
765 PaintConverter.getInstance(), Color.DODGERBLUE) {
766
767 @Override
768 public boolean isSettable(TextInputControl n) {
769 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
770 return skin.highlightFill == null || !skin.highlightFill.isBound();
771 }
772
773 @Override @SuppressWarnings("unchecked")
774 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
775 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
776 return (StyleableProperty<Paint>)skin.highlightFill;
777 }
778 };
779
780 private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_TEXT_FILL =
781 new CssMetaData<TextInputControl,Paint>("-fx-highlight-text-fill",
782 PaintConverter.getInstance(), Color.WHITE) {
783
784 @Override
785 public boolean isSettable(TextInputControl n) {
786 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
787 return skin.highlightTextFill == null || !skin.highlightTextFill.isBound();
788 }
789
790 @Override @SuppressWarnings("unchecked")
791 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
792 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
793 return (StyleableProperty<Paint>)skin.highlightTextFill;
794 }
795 };
796
797 private static final CssMetaData<TextInputControl,Boolean> DISPLAY_CARET =
798 new CssMetaData<TextInputControl,Boolean>("-fx-display-caret",
799 BooleanConverter.getInstance(), Boolean.TRUE) {
800
801 @Override
802 public boolean isSettable(TextInputControl n) {
803 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
804 return skin.displayCaret == null || !skin.displayCaret.isBound();
805 }
806
807 @Override @SuppressWarnings("unchecked")
808 public StyleableProperty<Boolean> getStyleableProperty(TextInputControl n) {
809 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
810 return (StyleableProperty<Boolean>)skin.displayCaret;
811 }
812 };
813
814 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
815 static {
816 List<CssMetaData<? extends Styleable, ?>> styleables =
817 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
818 styleables.add(TEXT_FILL);
819 styleables.add(PROMPT_TEXT_FILL);
820 styleables.add(HIGHLIGHT_FILL);
821 styleables.add(HIGHLIGHT_TEXT_FILL);
822 styleables.add(DISPLAY_CARET);
823
824 STYLEABLES = Collections.unmodifiableList(styleables);
825 }
826 }
827
828 /**
829 * @return The CssMetaData associated with this class, which may include the
830 * CssMetaData of its super classes.
831 */
832 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
833 return StyleableProperties.STYLEABLES;
834 }
835
836 /**
837 * {@inheritDoc}
838 */
839 @Override
840 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
841 return getClassCssMetaData();
842 }
843
844 protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
845 switch (action) {
846 case SHOW_TEXT_RANGE: {
847 Integer start = (Integer)parameters[0];
848 Integer end = (Integer)parameters[1];
849 if (start != null && end != null) {
850 scrollCharacterToVisible(end);
851 scrollCharacterToVisible(start);
852 scrollCharacterToVisible(end);
853 }
854 break;
855 }
856 default: super.executeAccessibleAction(action, parameters);
857 }
858 }
859 }
|
1 /*
2 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
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.Properties;
29 import com.sun.javafx.scene.control.skin.FXVK;
30 import com.sun.javafx.scene.input.ExtendedInputMethodRequests;
31 import javafx.animation.KeyFrame;
32 import javafx.animation.Timeline;
33 import javafx.application.ConditionalFeature;
34 import javafx.application.Platform;
35 import javafx.beans.binding.BooleanBinding;
36 import javafx.beans.binding.ObjectBinding;
37 import javafx.beans.property.BooleanProperty;
38 import javafx.beans.property.ObjectProperty;
39 import javafx.beans.property.SimpleBooleanProperty;
40 import javafx.beans.value.ObservableBooleanValue;
41 import javafx.beans.value.ObservableObjectValue;
42 import javafx.collections.ObservableList;
43 import javafx.css.CssMetaData;
44 import javafx.css.Styleable;
45 import javafx.css.StyleableBooleanProperty;
46 import javafx.css.StyleableObjectProperty;
47 import javafx.css.StyleableProperty;
48 import javafx.geometry.NodeOrientation;
49 import javafx.geometry.Point2D;
50 import javafx.geometry.Rectangle2D;
51 import javafx.scene.AccessibleAction;
52 import javafx.scene.Node;
53 import javafx.scene.Scene;
54 import javafx.scene.control.IndexRange;
55 import javafx.scene.control.SkinBase;
56 import javafx.scene.control.TextInputControl;
57 import javafx.scene.input.InputMethodEvent;
58 import javafx.scene.input.InputMethodHighlight;
59 import javafx.scene.input.InputMethodTextRun;
60 import javafx.scene.layout.StackPane;
61 import javafx.scene.paint.Color;
62 import javafx.scene.paint.Paint;
63 import javafx.scene.shape.ClosePath;
64 import javafx.scene.shape.HLineTo;
65 import javafx.scene.shape.Line;
66 import javafx.scene.shape.LineTo;
67 import javafx.scene.shape.MoveTo;
68 import javafx.scene.shape.Path;
69 import javafx.scene.shape.PathElement;
70 import javafx.scene.shape.Shape;
71 import javafx.scene.shape.VLineTo;
72 import javafx.stage.Window;
73 import javafx.util.Duration;
74 import java.lang.ref.WeakReference;
75 import java.util.ArrayList;
76 import java.util.Collections;
77 import java.util.List;
78 import com.sun.javafx.PlatformUtil;
79 import javafx.css.converter.BooleanConverter;
80 import javafx.css.converter.PaintConverter;
81 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
82 import com.sun.javafx.scene.text.HitInfo;
83 import com.sun.javafx.tk.FontMetrics;
84 import com.sun.javafx.tk.Toolkit;
85 import static com.sun.javafx.PlatformUtil.isWindows;
86 import java.security.AccessController;
87 import java.security.PrivilegedAction;
88
89 /**
90 * Abstract base class for text input skins.
91 *
92 * @since 9
93 * @see TextFieldSkin
94 * @see TextAreaSkin
95 */
96 public abstract class TextInputControlSkin<T extends TextInputControl> extends SkinBase<T> {
97
98 /**************************************************************************
99 *
100 * Static fields / blocks
101 *
102 **************************************************************************/
103
104 /**
105 * Unit names for caret movement.
106 *
107 * @see #moveCaret(TextUnit, Direction, boolean)
108 */
109 public static enum TextUnit { CHARACTER, WORD, LINE, PARAGRAPH, PAGE };
110
111 /**
112 * Direction names for caret movement.
113 *
114 * @see #moveCaret(TextUnit, Direction, boolean)
115 */
116 public static enum Direction { LEFT, RIGHT, UP, DOWN, BEGINNING, END };
117
118 static boolean preload = false;
119 static {
120 AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
121 String s = System.getProperty("com.sun.javafx.virtualKeyboard.preload");
122 if (s != null) {
123 if (s.equalsIgnoreCase("PRERENDER")) {
124 preload = true;
125 }
126 }
127 return null;
128 });
129 }
130
131 /**
132 * Specifies whether we ought to show handles. We should do it on touch platforms, but not
133 * iOS (and maybe not Android either?)
134 */
135 static final boolean SHOW_HANDLES = Properties.IS_TOUCH_SUPPORTED && !PlatformUtil.isIOS();
136
137 private final static boolean IS_FXVK_SUPPORTED = Platform.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD);
138
139 /**************************************************************************
140 *
141 * Private fields
142 *
143 **************************************************************************/
144
145 final ObservableObjectValue<FontMetrics> fontMetrics;
146 private ObservableBooleanValue caretVisible;
147 private CaretBlinking caretBlinking = new CaretBlinking(blinkProperty());
148
149 /**
150 * A path, provided by the textNode, which represents the caret.
151 * I assume this has to be updated whenever the caretPosition
152 * changes. Perhaps more frequently (including text changes),
153 * but I'm not sure.
154 */
155 final Path caretPath = new Path();
156
157 StackPane caretHandle = null;
158 StackPane selectionHandle1 = null;
159 StackPane selectionHandle2 = null;
160
161 // Start/Length of the text under input method composition
162 private int imstart;
163 private int imlength;
164 // Holds concrete attributes for the composition runs
165 private List<Shape> imattrs = new java.util.ArrayList<Shape>();
166
167
168
169 /**************************************************************************
170 *
171 * Constructors
172 *
173 **************************************************************************/
174
175 /**
176 * Creates a new instance of TextInputControlSkin, although note that this
177 * instance does not handle any behavior / input mappings - this needs to be
178 * handled appropriately by subclasses.
179 *
180 * @param control The control that this skin should be installed onto.
181 */
182 public TextInputControlSkin(final T control) {
183 super(control);
184
185 fontMetrics = new ObjectBinding<FontMetrics>() {
186 { bind(control.fontProperty()); }
187 @Override protected FontMetrics computeValue() {
188 invalidateMetrics();
189 return Toolkit.getToolkit().getFontLoader().getFontMetrics(control.getFont());
190 }
191 };
192
193 /**
194 * The caret is visible when the text box is focused AND when the selection
195 * is empty. If the selection is non empty or the text box is not focused
196 * then we don't want to show the caret. Also, we show the caret while
197 * performing some operations such as most key strokes. In that case we
198 * simply toggle its opacity.
199 * <p>
200 */
201 caretVisible = new BooleanBinding() {
202 { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
203 control.disabledProperty(), control.editableProperty(), displayCaret, blinkProperty());}
204 @Override protected boolean computeValue() {
205 // RT-10682: On Windows, we show the caret during selection, but on others we hide it
206 return !blinkProperty().get() && displayCaret.get() && control.isFocused() &&
207 (isWindows() || (control.getCaretPosition() == control.getAnchor())) &&
208 !control.isDisabled() &&
209 control.isEditable();
210 }
211 };
212
213 if (SHOW_HANDLES) {
214 caretHandle = new StackPane();
215 selectionHandle1 = new StackPane();
216 selectionHandle2 = new StackPane();
217
218 caretHandle.setManaged(false);
219 selectionHandle1.setManaged(false);
220 selectionHandle2.setManaged(false);
221
222 caretHandle.visibleProperty().bind(new BooleanBinding() {
223 { bind(control.focusedProperty(), control.anchorProperty(),
224 control.caretPositionProperty(), control.disabledProperty(),
225 control.editableProperty(), control.lengthProperty(), displayCaret);}
226 @Override protected boolean computeValue() {
227 return (displayCaret.get() && control.isFocused() &&
228 control.getCaretPosition() == control.getAnchor() &&
229 !control.isDisabled() && control.isEditable() &&
230 control.getLength() > 0);
231 }
232 });
233
234
235 selectionHandle1.visibleProperty().bind(new BooleanBinding() {
236 { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
237 control.disabledProperty(), displayCaret);}
238 @Override protected boolean computeValue() {
239 return (displayCaret.get() && control.isFocused() &&
240 control.getCaretPosition() != control.getAnchor() &&
241 !control.isDisabled());
242 }
243 });
244
245
246 selectionHandle2.visibleProperty().bind(new BooleanBinding() {
247 { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
248 control.disabledProperty(), displayCaret);}
249 @Override protected boolean computeValue() {
250 return (displayCaret.get() && control.isFocused() &&
251 control.getCaretPosition() != control.getAnchor() &&
252 !control.isDisabled());
253 }
254 });
255
256
257 caretHandle.getStyleClass().setAll("caret-handle");
258 selectionHandle1.getStyleClass().setAll("selection-handle");
259 selectionHandle2.getStyleClass().setAll("selection-handle");
260
261 selectionHandle1.setId("selection-handle-1");
262 selectionHandle2.setId("selection-handle-2");
263 }
264
265 if (IS_FXVK_SUPPORTED) {
266 if (preload) {
267 Scene scene = control.getScene();
268 if (scene != null) {
269 Window window = scene.getWindow();
270 if (window != null) {
271 FXVK.init(control);
272 }
273 }
274 }
275 control.focusedProperty().addListener(observable -> {
276 if (FXVK.useFXVK()) {
277 Scene scene = getSkinnable().getScene();
278 if (control.isEditable() && control.isFocused()) {
279 FXVK.attach(control);
280 } else if (scene == null ||
281 scene.getWindow() == null ||
282 !scene.getWindow().isFocused() ||
283 !(scene.getFocusOwner() instanceof TextInputControl &&
284 ((TextInputControl)scene.getFocusOwner()).isEditable())) {
285 FXVK.detach();
286 }
287 }
288 });
289 }
290
291 if (control.getOnInputMethodTextChanged() == null) {
292 control.setOnInputMethodTextChanged(event -> {
293 handleInputMethodEvent(event);
294 });
295 }
296
297 control.setInputMethodRequests(new ExtendedInputMethodRequests() {
298 @Override public Point2D getTextLocation(int offset) {
299 Scene scene = getSkinnable().getScene();
300 Window window = scene.getWindow();
301 // Don't use imstart here because it isn't initialized yet.
302 Rectangle2D characterBounds = getCharacterBounds(control.getSelection().getStart() + offset);
303 Point2D p = getSkinnable().localToScene(characterBounds.getMinX(), characterBounds.getMaxY());
304 Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(),
305 window.getY() + scene.getY() + p.getY());
306 return location;
307 }
308
309 @Override public int getLocationOffset(int x, int y) {
310 return getInsertionPoint(x, y);
311 }
312
313 @Override public void cancelLatestCommittedText() {
314 // TODO
315 }
316
317 @Override public String getSelectedText() {
318 TextInputControl control = getSkinnable();
319 IndexRange selection = control.getSelection();
320
321 return control.getText(selection.getStart(), selection.getEnd());
322 }
323
324 @Override public int getInsertPositionOffset() {
325 int caretPosition = getSkinnable().getCaretPosition();
326 if (caretPosition < imstart) {
327 return caretPosition;
328 } else if (caretPosition < imstart + imlength) {
329 return imstart;
330 } else {
331 return caretPosition - imlength;
332 }
333 }
334
335 @Override public String getCommittedText(int begin, int end) {
336 TextInputControl control = getSkinnable();
337 if (begin < imstart) {
338 if (end <= imstart) {
339 return control.getText(begin, end);
340 } else {
341 return control.getText(begin, imstart) + control.getText(imstart + imlength, end + imlength);
342 }
343 } else {
344 return control.getText(begin + imlength, end + imlength);
345 }
346 }
347
348 @Override public int getCommittedTextLength() {
349 return getSkinnable().getText().length() - imlength;
350 }
351 });
352 }
353
354
355
356 /**************************************************************************
357 *
358 * Properties
359 *
360 **************************************************************************/
361
362 // --- blink
363 private BooleanProperty blink;
364 private final void setBlink(boolean value) {
365 blinkProperty().set(value);
366 }
367 private final boolean isBlink() {
368 return blinkProperty().get();
369 }
370 private final BooleanProperty blinkProperty() {
371 if (blink == null) {
372 blink = new SimpleBooleanProperty(this, "blink", true);
373 }
374 return blink;
375 }
376
377 // --- text fill
378 /**
379 * The fill to use for the text under normal conditions
380 */
381 private final ObjectProperty<Paint> textFill = new StyleableObjectProperty<Paint>(Color.BLACK) {
382 @Override protected void invalidated() {
383 updateTextFill();
384 }
385
386 @Override public Object getBean() {
387 return TextInputControlSkin.this;
388 }
389
390 @Override public String getName() {
391 return "textFill";
392 }
393
394 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
395 return StyleableProperties.TEXT_FILL;
396 }
397 };
398
399 /**
400 * The fill {@code Paint} used for the foreground text color.
401 */
402 protected final void setTextFill(Paint value) {
403 textFill.set(value);
404 }
405 protected final Paint getTextFill() {
406 return textFill.get();
407 }
408 protected final ObjectProperty<Paint> textFillProperty() {
409 return textFill;
410 }
411
412 // --- prompt text fill
413 private final ObjectProperty<Paint> promptTextFill = new StyleableObjectProperty<Paint>(Color.GRAY) {
414 @Override public Object getBean() {
415 return TextInputControlSkin.this;
416 }
417
418 @Override public String getName() {
419 return "promptTextFill";
420 }
421
422 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
423 return StyleableProperties.PROMPT_TEXT_FILL;
424 }
425 };
426
427 /**
428 * The fill {@code Paint} used for the foreground prompt text color.
429 */
430 protected final void setPromptTextFill(Paint value) {
431 promptTextFill.set(value);
432 }
433 protected final Paint getPromptTextFill() {
434 return promptTextFill.get();
435 }
436 protected final ObjectProperty<Paint> promptTextFillProperty() {
437 return promptTextFill;
438 }
439
440 // --- hightlight fill
441 /**
442 * The fill to use for the text when highlighted.
443 */
444 private final ObjectProperty<Paint> highlightFill = new StyleableObjectProperty<Paint>(Color.DODGERBLUE) {
445 @Override protected void invalidated() {
446 updateHighlightFill();
447 }
448
449 @Override public Object getBean() {
450 return TextInputControlSkin.this;
451 }
452
453 @Override public String getName() {
454 return "highlightFill";
455 }
456
457 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
458 return StyleableProperties.HIGHLIGHT_FILL;
459 }
460 };
461
462 /**
463 * The fill {@code Paint} used for the background of selected text.
464 */
465 protected final void setHighlightFill(Paint value) {
466 highlightFill.set(value);
467 }
468 protected final Paint getHighlightFill() {
469 return highlightFill.get();
470 }
471 protected final ObjectProperty<Paint> highlightFillProperty() {
472 return highlightFill;
473 }
474
475 // --- highlight text fill
476 private final ObjectProperty<Paint> highlightTextFill = new StyleableObjectProperty<Paint>(Color.WHITE) {
477 @Override protected void invalidated() {
478 updateHighlightTextFill();
479 }
480
481 @Override public Object getBean() {
482 return TextInputControlSkin.this;
483 }
484
485 @Override public String getName() {
486 return "highlightTextFill";
487 }
488
489 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
490 return StyleableProperties.HIGHLIGHT_TEXT_FILL;
491 }
492 };
493
494 /**
495 * The fill {@code Paint} used for the foreground of selected text.
496 */
497 protected final void setHighlightTextFill(Paint value) {
498 highlightTextFill.set(value);
499 }
500 protected final Paint getHighlightTextFill() {
501 return highlightTextFill.get();
502 }
503 protected final ObjectProperty<Paint> highlightTextFillProperty() {
504 return highlightTextFill;
505 }
506
507 // --- display caret
508 private final BooleanProperty displayCaret = new StyleableBooleanProperty(true) {
509 @Override public Object getBean() {
510 return TextInputControlSkin.this;
511 }
512
513 @Override public String getName() {
514 return "displayCaret";
515 }
516
517 @Override public CssMetaData<TextInputControl,Boolean> getCssMetaData() {
518 return StyleableProperties.DISPLAY_CARET;
519 }
520 };
521
522 private final void setDisplayCaret(boolean value) {
523 displayCaret.set(value);
524 }
525 private final boolean isDisplayCaret() {
526 return displayCaret.get();
527 }
528 private final BooleanProperty displayCaretProperty() {
529 return displayCaret;
530 }
531
532
533 /**
534 * Caret bias in the content. true means a bias towards forward character
535 * (true=leading/false=trailing)
536 */
537 private BooleanProperty forwardBias = new SimpleBooleanProperty(this, "forwardBias", true);
538 protected final BooleanProperty forwardBiasProperty() {
539 return forwardBias;
540 }
541 // Public for behavior
542 public final void setForwardBias(boolean isLeading) {
543 forwardBias.set(isLeading);
544 }
545 protected final boolean isForwardBias() {
546 return forwardBias.get();
547 }
548
549
550
551 /**************************************************************************
552 *
553 * Abstract API
554 *
555 **************************************************************************/
556
557 /**
558 * @return the path elements describing the shape of the underline for the given range.
559 */
560 protected abstract PathElement[] getUnderlineShape(int start, int end);
561 /**
562 * @return the path elements describing the bounding rectangles for the given range of text.
563 */
564 protected abstract PathElement[] getRangeShape(int start, int end);
565 /**
566 * Adds highlight for composed text from Input Method.
567 */
568 protected abstract void addHighlight(List<? extends Node> nodes, int start);
569 /**
570 * Removes highlight for composed text from Input Method.
571 */
572 protected abstract void removeHighlight(List<? extends Node> nodes);
573
574 // Public for behavior
575 /**
576 * Moves the caret by one of the given text unit, in the given
577 * direction. Note that only certain combinations are valid,
578 * depending on the implementing subclass.
579 *
580 * @param unit the unit of text to move by.
581 * @param dir the direction of movement.
582 * @param select whether to extends the selection to the new posititon.
583 */
584 public abstract void moveCaret(TextUnit unit, Direction dir, boolean select);
585
586 /**************************************************************************
587 *
588 * Public API
589 *
590 **************************************************************************/
591
592
593 // Public for behavior
594 /**
595 * Returns the position to be used for a context menu, based on the location
596 * of the caret handle or selection handles. This is supported only on touch
597 * displays and does not use the location of the mouse.
598 */
599 public Point2D getMenuPosition() {
600 if (SHOW_HANDLES) {
601 if (caretHandle.isVisible()) {
602 return new Point2D(caretHandle.getLayoutX() + caretHandle.getWidth() / 2,
603 caretHandle.getLayoutY());
604 } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
605 return new Point2D((selectionHandle1.getLayoutX() + selectionHandle1.getWidth() / 2 +
606 selectionHandle2.getLayoutX() + selectionHandle2.getWidth() / 2) / 2,
607 selectionHandle2.getLayoutY() + selectionHandle2.getHeight() / 2);
608 } else {
609 return null;
610 }
611 } else {
612 throw new UnsupportedOperationException();
613 }
614 }
615
616 // For use with PasswordField in TextFieldSkin
617 /**
618 * This method may be overridden by subclasses to replace the displayed
619 * characters without affecting the actual text content. This is used to
620 * display bullet characters in PasswordField.
621 *
622 * @param txt the content that may need to be masked.
623 * @return the replacement string. This may just be the input string, or may be a string of replacement characters with the same length as the input string.
624 */
625 protected String maskText(String txt) {
626 return txt;
627 }
628
629 /**
630 * Returns the insertion point for a given location.
631 *
632 * @param x
633 * @param y
634 */
635 protected int getInsertionPoint(double x, double y) { return 0; }
636
637 /**
638 * Returns the bounds of the character at a given index.
639 *
640 * @param index
641 */
642 public Rectangle2D getCharacterBounds(int index) { return null; }
643
644 /**
645 * Ensures that the character at a given index is visible.
646 *
647 * @param index
648 */
649 protected void scrollCharacterToVisible(int index) {}
650
651 /**
652 * Invalidates cached min and pref sizes for the TextInputControl.
653 */
654 protected void invalidateMetrics() {
655 }
656
657 /**
658 * Called when textFill property changes.
659 */
660 protected void updateTextFill() {};
661
662 /**
663 * Called when highlightFill property changes.
664 */
665 protected void updateHighlightFill() {};
666
667 /**
668 * Called when highlightTextFill property changes.
669 */
670 protected void updateHighlightTextFill() {};
671
672 protected void handleInputMethodEvent(InputMethodEvent event) {
673 final TextInputControl textInput = getSkinnable();
674 if (textInput.isEditable() && !textInput.textProperty().isBound() && !textInput.isDisabled()) {
675
676 // just replace the text on iOS
677 if (PlatformUtil.isIOS()) {
678 textInput.setText(event.getCommitted());
679 return;
680 }
681
682 // remove previous input method text (if any) or selected text
683 if (imlength != 0) {
684 removeHighlight(imattrs);
685 imattrs.clear();
686 textInput.selectRange(imstart, imstart + imlength);
687 }
688
689 // Insert committed text
690 if (event.getCommitted().length() != 0) {
701 textInput.replaceText(textInput.getSelection(), composed.toString());
702 imlength = composed.length();
703 if (imlength != 0) {
704 int pos = imstart;
705 for (InputMethodTextRun run : event.getComposed()) {
706 int endPos = pos + run.getText().length();
707 createInputMethodAttributes(run.getHighlight(), pos, endPos);
708 pos = endPos;
709 }
710 addHighlight(imattrs, imstart);
711
712 // Set caret position in composed text
713 int caretPos = event.getCaretPosition();
714 if (caretPos >= 0 && caretPos < imlength) {
715 textInput.selectRange(imstart + caretPos, imstart + caretPos);
716 }
717 }
718 }
719 }
720
721 // Public for behavior
722 /**
723 * Starts or stops caret blinking. The behavior classes use this to temporarily
724 * pause blinking while user is typing or otherwise moving the caret.
725 *
726 * @param value whether caret should be blinking.
727 */
728 public void setCaretAnimating(boolean value) {
729 if (value) {
730 caretBlinking.start();
731 } else {
732 caretBlinking.stop();
733 blinkProperty().set(true);
734 }
735 }
736
737
738
739 /**************************************************************************
740 *
741 * Private implementation
742 *
743 **************************************************************************/
744
745 TextInputControlBehavior getBehavior() {
746 return null;
747 }
748
749 ObservableBooleanValue caretVisibleProperty() {
750 return caretVisible;
751 }
752
753 boolean isRTL() {
754 return (getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT);
755 };
756
757 private void createInputMethodAttributes(InputMethodHighlight highlight, int start, int end) {
758 double minX = 0f;
759 double maxX = 0f;
760 double minY = 0f;
761 double maxY = 0f;
762
763 PathElement elements[] = getUnderlineShape(start, end);
764 for (int i = 0; i < elements.length; i++) {
765 PathElement pe = elements[i];
766 if (pe instanceof MoveTo) {
767 minX = maxX = ((MoveTo)pe).getX();
768 minY = maxY = ((MoveTo)pe).getY();
769 } else if (pe instanceof LineTo) {
770 minX = (minX < ((LineTo)pe).getX() ? minX : ((LineTo)pe).getX());
771 maxX = (maxX > ((LineTo)pe).getX() ? maxX : ((LineTo)pe).getX());
772 minY = (minY < ((LineTo)pe).getY() ? minY : ((LineTo)pe).getY());
773 maxY = (maxY > ((LineTo)pe).getY() ? maxY : ((LineTo)pe).getY());
774 } else if (pe instanceof HLineTo) {
775 minX = (minX < ((HLineTo)pe).getX() ? minX : ((HLineTo)pe).getX());
801 } else if (highlight == InputMethodHighlight.SELECTED_CONVERTED) {
802 // thick underline.
803 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
804 attr.setStroke(textFill.get());
805 attr.setStrokeWidth((maxY - minY) * 3);
806 } else if (highlight == InputMethodHighlight.UNSELECTED_CONVERTED) {
807 // single underline.
808 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
809 attr.setStroke(textFill.get());
810 attr.setStrokeWidth(maxY - minY);
811 }
812
813 if (attr != null) {
814 attr.setManaged(false);
815 imattrs.add(attr);
816 }
817 }
818 }
819 }
820
821
822
823 /**************************************************************************
824 *
825 * Support classes
826 *
827 **************************************************************************/
828
829 private static final class CaretBlinking {
830 private final Timeline caretTimeline;
831 private final WeakReference<BooleanProperty> blinkPropertyRef;
832
833 public CaretBlinking(final BooleanProperty blinkProperty) {
834 blinkPropertyRef = new WeakReference<>(blinkProperty);
835
836 caretTimeline = new Timeline();
837 caretTimeline.setCycleCount(Timeline.INDEFINITE);
838 caretTimeline.getKeyFrames().addAll(
839 new KeyFrame(Duration.ZERO, e -> setBlink(false)),
840 new KeyFrame(Duration.seconds(.5), e -> setBlink(true)),
841 new KeyFrame(Duration.seconds(1)));
842 }
843
844 public void start() {
845 caretTimeline.play();
846 }
847
848 public void stop() {
849 caretTimeline.stop();
850 }
851
852 private void setBlink(final boolean value) {
853 final BooleanProperty blinkProperty = blinkPropertyRef.get();
854 if (blinkProperty == null) {
855 caretTimeline.stop();
856 return;
857 }
858
859 blinkProperty.set(value);
860 }
861 }
862
863
864 private static class StyleableProperties {
865 private static final CssMetaData<TextInputControl,Paint> TEXT_FILL =
866 new CssMetaData<TextInputControl,Paint>("-fx-text-fill",
867 PaintConverter.getInstance(), Color.BLACK) {
868
869 @Override public boolean isSettable(TextInputControl n) {
870 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
871 return skin.textFill == null || !skin.textFill.isBound();
872 }
873
874 @Override @SuppressWarnings("unchecked")
875 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
876 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
877 return (StyleableProperty<Paint>)skin.textFill;
878 }
879 };
880
881 private static final CssMetaData<TextInputControl,Paint> PROMPT_TEXT_FILL =
882 new CssMetaData<TextInputControl,Paint>("-fx-prompt-text-fill",
883 PaintConverter.getInstance(), Color.GRAY) {
884
885 @Override public boolean isSettable(TextInputControl n) {
886 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
887 return skin.promptTextFill == null || !skin.promptTextFill.isBound();
888 }
889
890 @Override @SuppressWarnings("unchecked")
891 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
892 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
893 return (StyleableProperty<Paint>)skin.promptTextFill;
894 }
895 };
896
897 private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_FILL =
898 new CssMetaData<TextInputControl,Paint>("-fx-highlight-fill",
899 PaintConverter.getInstance(), Color.DODGERBLUE) {
900
901 @Override public boolean isSettable(TextInputControl n) {
902 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
903 return skin.highlightFill == null || !skin.highlightFill.isBound();
904 }
905
906 @Override @SuppressWarnings("unchecked")
907 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
908 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
909 return (StyleableProperty<Paint>)skin.highlightFill;
910 }
911 };
912
913 private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_TEXT_FILL =
914 new CssMetaData<TextInputControl,Paint>("-fx-highlight-text-fill",
915 PaintConverter.getInstance(), Color.WHITE) {
916
917 @Override public boolean isSettable(TextInputControl n) {
918 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
919 return skin.highlightTextFill == null || !skin.highlightTextFill.isBound();
920 }
921
922 @Override @SuppressWarnings("unchecked")
923 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
924 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
925 return (StyleableProperty<Paint>)skin.highlightTextFill;
926 }
927 };
928
929 private static final CssMetaData<TextInputControl,Boolean> DISPLAY_CARET =
930 new CssMetaData<TextInputControl,Boolean>("-fx-display-caret",
931 BooleanConverter.getInstance(), Boolean.TRUE) {
932
933 @Override public boolean isSettable(TextInputControl n) {
934 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
935 return skin.displayCaret == null || !skin.displayCaret.isBound();
936 }
937
938 @Override @SuppressWarnings("unchecked")
939 public StyleableProperty<Boolean> getStyleableProperty(TextInputControl n) {
940 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
941 return (StyleableProperty<Boolean>)skin.displayCaret;
942 }
943 };
944
945 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
946 static {
947 List<CssMetaData<? extends Styleable, ?>> styleables =
948 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
949 styleables.add(TEXT_FILL);
950 styleables.add(PROMPT_TEXT_FILL);
951 styleables.add(HIGHLIGHT_FILL);
952 styleables.add(HIGHLIGHT_TEXT_FILL);
953 styleables.add(DISPLAY_CARET);
954
955 STYLEABLES = Collections.unmodifiableList(styleables);
956 }
957 }
958
959 /**
960 * Returns the CssMetaData associated with this class, which may include the
961 * CssMetaData of its super classes.
962 */
963 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
964 return StyleableProperties.STYLEABLES;
965 }
966
967 /**
968 * {@inheritDoc}
969 */
970 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
971 return getClassCssMetaData();
972 }
973
974 @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
975 switch (action) {
976 case SHOW_TEXT_RANGE: {
977 Integer start = (Integer)parameters[0];
978 Integer end = (Integer)parameters[1];
979 if (start != null && end != null) {
980 scrollCharacterToVisible(end);
981 scrollCharacterToVisible(start);
982 scrollCharacterToVisible(end);
983 }
984 break;
985 }
986 default: super.executeAccessibleAction(action, parameters);
987 }
988 }
989
990 /**
991 * This class represents the hit information for a Text node.
992 */
993 public static class TextPosInfo {
994
995 TextPosInfo(HitInfo hit) {
996 this(hit.getCharIndex(), hit.isLeading());
997 }
998
999 /**
1000 * Create a TextPosInfo object representing a text index and forward bias.
1001 *
1002 * @param charIndex the character index.
1003 * @param leading whether the hit is on the leading edge of the character. If it is false, it represents the trailing edge.
1004 */
1005 public TextPosInfo(int charIndex, boolean leading) {
1006 setCharIndex(charIndex);
1007 setLeading(leading);
1008 }
1009
1010 /**
1011 * The index of the character which this hit information refers to.
1012 */
1013 private int charIndex;
1014 public int getCharIndex() { return charIndex; }
1015 void setCharIndex(int charIndex) { this.charIndex = charIndex; }
1016
1017 /**
1018 * Indicates whether the hit is on the leading edge of the character.
1019 * If it is false, it represents the trailing edge.
1020 */
1021 private boolean leading;
1022 public boolean isLeading() { return leading; }
1023 void setLeading(boolean leading) { this.leading = leading; }
1024
1025 /**
1026 * Returns the index of the insertion position.
1027 */
1028 public int getInsertionIndex() {
1029 return leading ? charIndex : charIndex + 1;
1030 }
1031
1032 @Override public String toString() {
1033 return "charIndex: " + charIndex + ", isLeading: " + leading;
1034 }
1035 }
1036 }
|