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) { 510 String committed = event.getCommitted(); 511 textInput.replaceText(textInput.getSelection(), committed); 512 } 513 514 // Replace composed text 515 imstart = textInput.getSelection().getStart(); 516 StringBuilder composed = new StringBuilder(); 517 for (InputMethodTextRun run : event.getComposed()) { 518 composed.append(run.getText()); 519 } 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()); 565 maxX = (maxX > ((HLineTo)pe).getX() ? maxX : ((HLineTo)pe).getX()); 566 } else if (pe instanceof VLineTo) { 567 minY = (minY < ((VLineTo)pe).getY() ? minY : ((VLineTo)pe).getY()); 568 maxY = (maxY > ((VLineTo)pe).getY() ? maxY : ((VLineTo)pe).getY()); 569 } 570 // Don't assume that shapes are ended with ClosePath. 571 if (pe instanceof ClosePath || 572 i == elements.length - 1 || 573 (i < elements.length - 1 && elements[i+1] instanceof MoveTo)) { 574 // Now, create the attribute. 575 Shape attr = null; 576 if (highlight == InputMethodHighlight.SELECTED_RAW) { 577 // blue background 578 attr = new Path(); 579 ((Path)attr).getElements().addAll(getRangeShape(start, end)); 580 attr.setFill(Color.BLUE); 581 attr.setOpacity(0.3f); 582 } else if (highlight == InputMethodHighlight.UNSELECTED_RAW) { 583 // dash underline. 584 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1); 585 attr.setStroke(textFill.get()); 586 attr.setStrokeWidth(maxY - minY); 587 ObservableList<Double> dashArray = attr.getStrokeDashArray(); 588 dashArray.add(Double.valueOf(2f)); 589 dashArray.add(Double.valueOf(2f)); 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 }