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 package com.sun.javafx.scene.control.behavior;
  26 
  27 import com.sun.javafx.PlatformUtil;
  28 import com.sun.javafx.application.PlatformImpl;
  29 import com.sun.javafx.scene.control.Properties;
  30 import com.sun.javafx.scene.control.skin.FXVK;
  31 
  32 import javafx.event.ActionEvent;
  33 import javafx.event.Event;
  34 import javafx.event.EventHandler;
  35 import javafx.scene.control.skin.TextInputControlSkin;
  36 import javafx.application.ConditionalFeature;
  37 import javafx.beans.InvalidationListener;
  38 import javafx.collections.ObservableList;
  39 import javafx.geometry.NodeOrientation;
  40 import javafx.scene.control.ContextMenu;
  41 import javafx.scene.control.IndexRange;
  42 import javafx.scene.control.MenuItem;
  43 import javafx.scene.control.PasswordField;
  44 import javafx.scene.control.SeparatorMenuItem;
  45 import javafx.scene.control.TextInputControl;
  46 import javafx.scene.input.ContextMenuEvent;
  47 import javafx.scene.input.Clipboard;
  48 import com.sun.javafx.scene.control.inputmap.InputMap;
  49 import com.sun.javafx.scene.control.inputmap.KeyBinding;
  50 import javafx.scene.input.KeyCode;
  51 import javafx.scene.input.KeyEvent;
  52 import javafx.scene.input.MouseEvent;
  53 
  54 import java.text.Bidi;
  55 import java.util.function.Predicate;
  56 
  57 import static com.sun.javafx.PlatformUtil.isLinux;
  58 import static com.sun.javafx.PlatformUtil.isMac;
  59 import static com.sun.javafx.PlatformUtil.isWindows;
  60 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
  61 import static javafx.scene.control.skin.TextInputControlSkin.TextUnit;
  62 import static javafx.scene.control.skin.TextInputControlSkin.Direction;
  63 import static com.sun.javafx.scene.control.inputmap.InputMap.KeyMapping;
  64 import static com.sun.javafx.scene.control.inputmap.InputMap.MouseMapping;
  65 import static javafx.scene.input.KeyCode.*;
  66 import static javafx.scene.input.KeyEvent.*;
  67 
  68 /**
  69  * All of the "button" types (CheckBox, RadioButton, ToggleButton, and Button)
  70  * and also maybe some other types like hyperlinks operate on the "armed"
  71  * selection strategy, just like JButton. This behavior class encapsulates that
  72  * logic in a way that can be reused and extended by each of the individual
  73  * class behaviors.
  74  *
  75  */
  76 public abstract class TextInputControlBehavior<T extends TextInputControl> extends BehaviorBase<T> {
  77 
  78     /**
  79      * Specifies whether we ought to show handles. We should do it on touch platforms, but not
  80      * iOS (and maybe not Android either?)
  81      */
  82     static final boolean SHOW_HANDLES = Properties.IS_TOUCH_SUPPORTED && !PlatformUtil.isIOS();
  83 
  84     /**************************************************************************
  85      * Fields                                                                 *
  86      *************************************************************************/
  87 
  88     final T textInputControl;
  89 
  90     /**
  91      * Used to keep track of the most recent key event. This is used when
  92      * handling InputCharacter actions.
  93      */
  94     private KeyEvent lastEvent;
  95 
  96     private InvalidationListener textListener = observable -> {
  97         invalidateBidi();
  98     };
  99 
 100     private final InputMap<T> inputMap;
 101 
 102 
 103 
 104 
 105     /***************************************************************************
 106      *                                                                         *
 107      * Constructors                                                            *
 108      *                                                                         *
 109      **************************************************************************/
 110 
 111     public TextInputControlBehavior(T c) {
 112         super(c);
 113 
 114         this.textInputControl = c;
 115 
 116         textInputControl.textProperty().addListener(textListener);
 117 
 118         // create a map for text input-specific mappings (this reuses the default
 119         // InputMap installed on the control, if it is non-null, allowing us to pick up any user-specified mappings)
 120         inputMap = createInputMap();
 121 
 122         // some of the mappings are only valid when the control is editable, or
 123         // only on certain platforms, so we create the following predicates that filters out the mapping when the
 124         // control is not in the correct state / on the correct platform
 125         final Predicate<KeyEvent> validWhenEditable = e -> !c.isEditable();
 126         final Predicate<KeyEvent> validOnMac = e -> !PlatformUtil.isMac();
 127         final Predicate<KeyEvent> validOnWindows = e -> !PlatformUtil.isWindows();
 128         final Predicate<KeyEvent> validOnLinux = e -> !PlatformUtil.isLinux();
 129 
 130         // create a child input map for mappings which are applicable on all
 131         // platforms, and regardless of editing state
 132         addDefaultMapping(inputMap,
 133                 // caret movement
 134                 keyMapping(RIGHT, e -> nextCharacterVisually(true)),
 135                 keyMapping(LEFT, e -> nextCharacterVisually(false)),
 136                 keyMapping(UP, e -> c.home()),
 137                 keyMapping(HOME, e -> c.home()),
 138                 keyMapping(DOWN, e -> c.end()),
 139                 keyMapping(END, e -> c.end()),
 140                 keyMapping(ENTER, this::fire),
 141 
 142                 keyMapping(new KeyBinding(HOME).shortcut(), e -> c.home()),
 143                 keyMapping(new KeyBinding(END).shortcut(), e -> c.end()),
 144 
 145                 // deletion (only applies when control is editable)
 146                 keyMapping(new KeyBinding(BACK_SPACE), e -> deletePreviousChar(), validWhenEditable),
 147                 keyMapping(new KeyBinding(BACK_SPACE).shift(), e -> deletePreviousChar(), validWhenEditable),
 148                 keyMapping(new KeyBinding(DELETE), e -> deleteNextChar(), validWhenEditable),
 149                 keyMapping(new KeyBinding(DELETE).shift(), e -> deleteNextChar(), validWhenEditable),
 150 
 151                 // cut (only applies when control is editable)
 152                 keyMapping(new KeyBinding(X).shortcut(), e -> c.cut(), validWhenEditable),
 153                 keyMapping(new KeyBinding(CUT), e -> cut(), validWhenEditable),
 154                 keyMapping(new KeyBinding(DELETE).shift(), e -> cut(), validWhenEditable),
 155 
 156                 // copy
 157                 keyMapping(new KeyBinding(C).shortcut(), e -> c.copy()),
 158                 keyMapping(new KeyBinding(INSERT).shortcut(), e -> c.copy()),
 159                 keyMapping(COPY, e -> c.copy()),
 160 
 161                 // paste (only applies when control is editable)
 162                 keyMapping(new KeyBinding(V).shortcut(), e -> c.paste(), validWhenEditable),
 163                 keyMapping(new KeyBinding(PASTE), e -> paste(), validWhenEditable),
 164                 keyMapping(new KeyBinding(INSERT).shift(), e -> paste(), validWhenEditable),
 165 
 166                 // selection
 167                 keyMapping(new KeyBinding(RIGHT).shift(), e -> selectRight()),
 168                 keyMapping(new KeyBinding(LEFT).shift(), e -> selectLeft()),
 169                 keyMapping(new KeyBinding(UP).shift(), e -> selectHome()),
 170                 keyMapping(new KeyBinding(DOWN).shift(), e -> selectEnd()),
 171                 keyMapping(new KeyBinding(HOME).shortcut().shift(), e -> selectHome()),
 172                 keyMapping(new KeyBinding(END).shortcut().shift(), e -> selectEnd()),
 173                 keyMapping(new KeyBinding(LEFT).shortcut().shift(), e -> selectHomeExtend()),
 174                 keyMapping(new KeyBinding(RIGHT).shortcut().shift(), e -> selectEndExtend()),
 175                 keyMapping(new KeyBinding(A).shortcut(), e -> c.selectAll()),
 176 
 177                 // Traversal Bindings
 178                 new KeyMapping(new KeyBinding(TAB), FocusTraversalInputMap::traverseNext),
 179                 new KeyMapping(new KeyBinding(TAB).shift(), FocusTraversalInputMap::traversePrevious),
 180                 new KeyMapping(new KeyBinding(TAB).ctrl(), FocusTraversalInputMap::traverseNext),
 181                 new KeyMapping(new KeyBinding(TAB).ctrl().shift(), FocusTraversalInputMap::traversePrevious),
 182 
 183                 // The following keys are forwarded to the parent container
 184                 new KeyMapping(ESCAPE, this::cancelEdit),
 185                 new KeyMapping(F10, this::forwardToParent),
 186 
 187                 // Linux specific mappings
 188                 keyMapping(new KeyBinding(Z).ctrl(), e -> undo(), validOnLinux),
 189                 keyMapping(new KeyBinding(Z).ctrl().shift(), e -> redo(), validOnLinux),
 190 
 191                 // character input.
 192                 // Any other key press first goes to normal text input
 193                 // Note this is KEY_TYPED because otherwise the character is not available in the event.
 194                 keyMapping(new KeyBinding(null, KEY_TYPED), this::defaultKeyTyped),
 195 
 196                 // However, we want to consume other key press / release events too, for
 197                 // things that would have been handled by the InputCharacter normally
 198                 keyMapping(new KeyBinding(null, KEY_PRESSED), e -> e.consume()),
 199 
 200                 // VK
 201                 new KeyMapping(new KeyBinding(DIGIT9).ctrl().shift(), e -> {
 202                     FXVK.toggleUseVK(textInputControl);
 203                 }, p -> !PlatformImpl.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD)),
 204 
 205                 // mouse and context menu mappings
 206                 new MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed),
 207                 new MouseMapping(MouseEvent.MOUSE_DRAGGED, this::mouseDragged),
 208                 new MouseMapping(MouseEvent.MOUSE_RELEASED, this::mouseReleased),
 209                 new InputMap.Mapping<ContextMenuEvent>(ContextMenuEvent.CONTEXT_MENU_REQUESTED, this::contextMenuRequested) {
 210                     @Override public int getSpecificity(Event event) {
 211                         return 1;
 212                     }
 213                 }
 214         );
 215 
 216         // mac os specific mappings
 217         InputMap<T> macOsInputMap = new InputMap<>(c);
 218         macOsInputMap.setInterceptor(e -> !PlatformUtil.isMac());
 219         macOsInputMap.getMappings().addAll(
 220             // Mac OS specific mappings
 221             keyMapping(new KeyBinding(HOME).shift(), e -> selectHomeExtend()),
 222             keyMapping(new KeyBinding(END).shift(), e -> selectEndExtend()),
 223             keyMapping(new KeyBinding(LEFT).shortcut(), e -> c.home()),
 224             keyMapping(new KeyBinding(RIGHT).shortcut(), e -> c.end()),
 225             keyMapping(new KeyBinding(LEFT).alt(), e -> leftWord()),
 226             keyMapping(new KeyBinding(RIGHT).alt(), e -> rightWord()),
 227             keyMapping(new KeyBinding(DELETE).alt(), e -> deleteNextWord()),
 228             keyMapping(new KeyBinding(BACK_SPACE).alt(), e -> deletePreviousWord()),
 229             keyMapping(new KeyBinding(BACK_SPACE).shortcut(), e -> deleteFromLineStart()),
 230             keyMapping(new KeyBinding(Z).shortcut(), e -> undo()),
 231             keyMapping(new KeyBinding(Z).shortcut().shift(), e -> redo()),
 232 
 233             // Mac OS specific selection mappings
 234             keyMapping(new KeyBinding(LEFT).shift().alt(), e -> selectLeftWord()),
 235             keyMapping(new KeyBinding(RIGHT).shift().alt(), e -> selectRightWord())
 236         );
 237         addDefaultChildMap(inputMap, macOsInputMap);
 238 
 239         // windows / linux specific mappings
 240         InputMap<T> nonMacOsInputMap = new InputMap<>(c);
 241         nonMacOsInputMap.setInterceptor(e -> PlatformUtil.isMac());
 242         nonMacOsInputMap.getMappings().addAll(
 243             keyMapping(new KeyBinding(HOME).shift(), e -> selectHome()),
 244             keyMapping(new KeyBinding(END).shift(), e -> selectEnd()),
 245             keyMapping(new KeyBinding(LEFT).ctrl(), e -> leftWord()),
 246             keyMapping(new KeyBinding(RIGHT).ctrl(), e -> rightWord()),
 247             keyMapping(new KeyBinding(H).ctrl(), e -> deletePreviousChar()),
 248             keyMapping(new KeyBinding(DELETE).ctrl(), e -> deleteNextWord()),
 249             keyMapping(new KeyBinding(BACK_SPACE).ctrl(), e -> deletePreviousWord()),
 250             keyMapping(new KeyBinding(BACK_SLASH).ctrl(), e -> c.deselect()),
 251             keyMapping(new KeyBinding(Z).ctrl(), e -> undo()),
 252             keyMapping(new KeyBinding(Y).ctrl(), e -> redo())
 253         );
 254         addDefaultChildMap(inputMap, nonMacOsInputMap);
 255 
 256         addKeyPadMappings(inputMap);
 257 
 258         textInputControl.textProperty().addListener(textListener);
 259     }
 260 
 261     @Override public InputMap<T> getInputMap() {
 262         return inputMap;
 263     }
 264 
 265     /**
 266      * Bind keypad arrow keys to the same as the regular arrow keys.
 267      */
 268     protected void addKeyPadMappings(InputMap<T> map) {
 269         // First create a temporary map for the keypad mappings
 270         InputMap<T> tmpMap = new InputMap<>(getNode());
 271         for (Object o : map.getMappings()) {
 272             if (o instanceof KeyMapping) {
 273                 KeyMapping mapping = (KeyMapping)o;
 274                 KeyBinding kb = (KeyBinding)mapping.getMappingKey();
 275                 if (kb.getCode() != null) {
 276                     KeyCode newCode = null;
 277                     switch (kb.getCode()) {
 278                         case LEFT:  newCode = KP_LEFT;  break;
 279                         case RIGHT: newCode = KP_RIGHT; break;
 280                         case UP:    newCode = KP_UP;    break;
 281                         case DOWN:  newCode = KP_DOWN;  break;
 282                     }
 283                     if (newCode != null) {
 284                         KeyBinding newkb = new KeyBinding(newCode).shift(kb.getShift())
 285                                                                   .ctrl(kb.getCtrl())
 286                                                                   .alt(kb.getAlt())
 287                                                                   .meta(kb.getMeta());
 288                         tmpMap.getMappings().add(new KeyMapping(newkb, mapping.getEventHandler()));
 289                     }
 290                 }
 291             }
 292         }
 293         // Install mappings
 294         for (Object o : tmpMap.getMappings()) {
 295             map.getMappings().add((KeyMapping)o);
 296         }
 297 
 298         // Recursive call for child maps
 299         for (Object o : map.getChildInputMaps()) {
 300             addKeyPadMappings((InputMap<T>)o);
 301         }
 302     }
 303 
 304 
 305     /**
 306      * Wraps the event handler to pause caret blinking when
 307      * processing the key event.
 308      */
 309     protected KeyMapping keyMapping(final KeyCode keyCode, final EventHandler<KeyEvent> eventHandler) {
 310         return keyMapping(new KeyBinding(keyCode), eventHandler);
 311     }
 312 
 313     protected KeyMapping keyMapping(KeyBinding keyBinding, final EventHandler<KeyEvent> eventHandler) {
 314         return keyMapping(keyBinding, eventHandler, null);
 315     }
 316 
 317     protected KeyMapping keyMapping(KeyBinding keyBinding, final EventHandler<KeyEvent> eventHandler, 
 318                                     Predicate<KeyEvent> interceptor) {
 319         return new KeyMapping(keyBinding,
 320                               e -> {
 321                                   setCaretAnimating(false);
 322                                   eventHandler.handle(e);
 323                                   setCaretAnimating(true);
 324                               },
 325                               interceptor);
 326     }
 327 
 328 
 329 
 330 
 331 
 332     /**************************************************************************
 333      * Disposal methods                                                       *
 334      *************************************************************************/
 335 
 336     @Override public void dispose() {
 337         textInputControl.textProperty().removeListener(textListener);
 338         super.dispose();
 339     }
 340 
 341     /**************************************************************************
 342      * Abstract methods                                                       *
 343      *************************************************************************/
 344 
 345     protected abstract void deleteChar(boolean previous);
 346     protected abstract void replaceText(int start, int end, String txt);
 347     protected abstract void setCaretAnimating(boolean play);
 348     protected abstract void deleteFromLineStart();
 349 
 350     protected abstract void mousePressed(MouseEvent e);
 351     protected abstract void mouseDragged(MouseEvent e);
 352     protected abstract void mouseReleased(MouseEvent e);
 353     protected abstract void contextMenuRequested(ContextMenuEvent e);
 354 
 355     /**************************************************************************
 356      * Key handling implementation                                            *
 357      *************************************************************************/
 358 
 359     /**
 360      * The default handler for a key typed event, which is called when none of
 361      * the other key bindings match. This is the method which handles basic
 362      * text entry.
 363      * @param event not null
 364      */
 365     private void defaultKeyTyped(KeyEvent event) {
 366         final TextInputControl textInput = getNode();
 367         // I'm not sure this case can actually ever happen, maybe this
 368         // should be an assert instead?
 369         if (!textInput.isEditable() || textInput.isDisabled()) return;
 370 
 371         // Sometimes we get events with no key character, in which case
 372         // we need to bail.
 373         String character = event.getCharacter();
 374         if (character.length() == 0) return;
 375 
 376         // Filter out control keys except control+Alt on PC or Alt on Mac
 377         if (event.isControlDown() || event.isAltDown() || (isMac() && event.isMetaDown())) {
 378             if (!((event.isControlDown() || isMac()) && event.isAltDown())) return;
 379         }
 380 
 381         setEditing(true);
 382 
 383         // Ignore characters in the control range and the ASCII delete
 384         // character as well as meta key presses
 385         if (character.charAt(0) > 0x1F
 386                 && character.charAt(0) != 0x7F
 387                 && !event.isMetaDown()) { // Not sure about this one
 388             final IndexRange selection = textInput.getSelection();
 389             final int start = selection.getStart();
 390             final int end = selection.getEnd();
 391 
 392             replaceText(start, end, character);
 393         }
 394 
 395         setEditing(false);
 396     }
 397 
 398     private Bidi bidi = null;
 399     private Boolean mixed = null;
 400     private Boolean rtlText = null;
 401 
 402     private void invalidateBidi() {
 403         bidi = null;
 404         mixed = null;
 405         rtlText = null;
 406     }
 407 
 408     private Bidi getBidi() {
 409         if (bidi == null) {
 410             bidi = new Bidi(textInputControl.textProperty().getValueSafe(),
 411                     (textInputControl.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT)
 412                             ? Bidi.DIRECTION_RIGHT_TO_LEFT
 413                             : Bidi.DIRECTION_LEFT_TO_RIGHT);
 414         }
 415         return bidi;
 416     }
 417 
 418     protected boolean isMixed() {
 419         if (mixed == null) {
 420             mixed = getBidi().isMixed();
 421         }
 422         return mixed;
 423     }
 424 
 425     protected boolean isRTLText() {
 426         if (rtlText == null) {
 427             Bidi bidi = getBidi();
 428             rtlText =
 429                     (bidi.isRightToLeft() ||
 430                             (isMixed() &&
 431                                     textInputControl.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT));
 432         }
 433         return rtlText;
 434     }
 435 
 436     private void nextCharacterVisually(boolean moveRight) {
 437         if (isMixed()) {
 438             TextInputControlSkin<?> skin = (TextInputControlSkin<?>)textInputControl.getSkin();
 439             skin.moveCaret(TextUnit.CHARACTER, moveRight ? Direction.RIGHT : Direction.LEFT, false);
 440         } else if (moveRight != isRTLText()) {
 441             textInputControl.forward();
 442         } else {
 443             textInputControl.backward();
 444         }
 445     }
 446 
 447     private void selectLeft() {
 448         if (isRTLText()) {
 449             textInputControl.selectForward();
 450         } else {
 451             textInputControl.selectBackward();
 452         }
 453     }
 454 
 455     private void selectRight() {
 456         if (isRTLText()) {
 457             textInputControl.selectBackward();
 458         } else {
 459             textInputControl.selectForward();
 460         }
 461     }
 462 
 463     private void deletePreviousChar() {
 464         setEditing(true);
 465         deleteChar(true);
 466         setEditing(false);
 467     }
 468 
 469     private void deleteNextChar() {
 470         setEditing(true);
 471         deleteChar(false);
 472         setEditing(false);
 473     }
 474 
 475     protected void deletePreviousWord() {
 476         setEditing(true);
 477         TextInputControl textInputControl = getNode();
 478         int end = textInputControl.getCaretPosition();
 479 
 480         if (end > 0) {
 481             textInputControl.previousWord();
 482             int start = textInputControl.getCaretPosition();
 483             replaceText(start, end, "");
 484         }
 485         setEditing(false);
 486     }
 487 
 488     protected void deleteNextWord() {
 489         setEditing(true);
 490         TextInputControl textInputControl = getNode();
 491         int start = textInputControl.getCaretPosition();
 492 
 493         if (start < textInputControl.getLength()) {
 494             nextWord();
 495             int end = textInputControl.getCaretPosition();
 496             replaceText(start, end, "");
 497         }
 498         setEditing(false);
 499     }
 500 
 501     public void deleteSelection() {
 502         setEditing(true);
 503         TextInputControl textInputControl = getNode();
 504         IndexRange selection = textInputControl.getSelection();
 505 
 506         if (selection.getLength() > 0) {
 507             deleteChar(false);
 508         }
 509         setEditing(false);
 510     }
 511 
 512     public void cut() {
 513         setEditing(true);
 514         getNode().cut();
 515         setEditing(false);
 516     }
 517 
 518     public void paste() {
 519         setEditing(true);
 520         getNode().paste();
 521         setEditing(false);
 522     }
 523 
 524     public void undo() {
 525         setEditing(true);
 526         getNode().undo();
 527         setEditing(false);
 528     }
 529 
 530     public void redo() {
 531         setEditing(true);
 532         getNode().redo();
 533         setEditing(false);
 534     }
 535 
 536     protected void selectPreviousWord() {
 537         getNode().selectPreviousWord();
 538     }
 539 
 540     public void selectNextWord() {
 541         TextInputControl textInputControl = getNode();
 542         if (isMac() || isLinux()) {
 543             textInputControl.selectEndOfNextWord();
 544         } else {
 545             textInputControl.selectNextWord();
 546         }
 547     }
 548 
 549     private void selectLeftWord() {
 550         if (isRTLText()) {
 551             selectNextWord();
 552         } else {
 553             selectPreviousWord();
 554         }
 555     }
 556 
 557     private void selectRightWord() {
 558         if (isRTLText()) {
 559             selectPreviousWord();
 560         } else {
 561             selectNextWord();
 562         }
 563     }
 564 
 565     protected void selectWord() {
 566         final TextInputControl textInputControl = getNode();
 567         textInputControl.previousWord();
 568         if (isWindows()) {
 569             textInputControl.selectNextWord();
 570         } else {
 571             textInputControl.selectEndOfNextWord();
 572         }
 573     }
 574 
 575     protected void previousWord() {
 576         getNode().previousWord();
 577     }
 578 
 579     protected void nextWord() {
 580         TextInputControl textInputControl = getNode();
 581         if (isMac() || isLinux()) {
 582             textInputControl.endOfNextWord();
 583         } else {
 584             textInputControl.nextWord();
 585         }
 586     }
 587 
 588     private void leftWord() {
 589         if (isRTLText()) {
 590             nextWord();
 591         } else {
 592             previousWord();
 593         }
 594     }
 595 
 596     private void rightWord() {
 597         if (isRTLText()) {
 598             previousWord();
 599         } else {
 600             nextWord();
 601         }
 602     }
 603 
 604     protected void fire(KeyEvent event) { } // TODO move to TextFieldBehavior
 605     protected void cancelEdit(KeyEvent event) { forwardToParent(event);}
 606 
 607     protected void forwardToParent(KeyEvent event) {
 608         if (getNode().getParent() != null) {
 609             getNode().getParent().fireEvent(event);
 610         }
 611     }
 612 
 613     protected void selectHome() {
 614         getNode().selectHome();
 615     }
 616 
 617     protected void selectEnd() {
 618         getNode().selectEnd();
 619     }
 620 
 621     protected void selectHomeExtend() {
 622         getNode().extendSelection(0);
 623     }
 624 
 625     protected void selectEndExtend() {
 626         TextInputControl textInputControl = getNode();
 627         textInputControl.extendSelection(textInputControl.getLength());
 628     }
 629 
 630     private boolean editing = false;
 631     protected void setEditing(boolean b) {
 632         editing = b;
 633     }
 634     public boolean isEditing() {
 635         return editing;
 636     }
 637 
 638     protected void populateContextMenu(ContextMenu contextMenu) {
 639         TextInputControl textInputControl = getNode();
 640         boolean editable = textInputControl.isEditable();
 641         boolean hasText = (textInputControl.getLength() > 0);
 642         boolean hasSelection = (textInputControl.getSelection().getLength() > 0);
 643         boolean maskText = (textInputControl instanceof PasswordField); // (maskText("A") != "A");
 644         ObservableList<MenuItem> items = contextMenu.getItems();
 645 
 646         if (SHOW_HANDLES) {
 647             items.clear();
 648             if (!maskText && hasSelection) {
 649                 if (editable) {
 650                     items.add(cutMI);
 651                 }
 652                 items.add(copyMI);
 653             }
 654             if (editable && Clipboard.getSystemClipboard().hasString()) {
 655                 items.add(pasteMI);
 656             }
 657             if (hasText) {
 658                 if (!hasSelection) {
 659                     items.add(selectWordMI);
 660                 }
 661                 items.add(selectAllMI);
 662             }
 663             selectWordMI.getProperties().put("refreshMenu", Boolean.TRUE);
 664             selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
 665         } else {
 666             if (editable) {
 667                 items.setAll(undoMI, redoMI, cutMI, copyMI, pasteMI, deleteMI,
 668                         separatorMI, selectAllMI);
 669             } else {
 670                 items.setAll(copyMI, separatorMI, selectAllMI);
 671             }
 672             undoMI.setDisable(!getNode().isUndoable());
 673             redoMI.setDisable(!getNode().isRedoable());
 674             cutMI.setDisable(maskText || !hasSelection);
 675             copyMI.setDisable(maskText || !hasSelection);
 676             pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString());
 677             deleteMI.setDisable(!hasSelection);
 678         }
 679     }
 680 
 681     private static class ContextMenuItem extends MenuItem {
 682         ContextMenuItem(final String action, EventHandler<ActionEvent> onAction) {
 683             super(getString("TextInputControl.menu." + action));
 684             setOnAction(onAction);
 685         }
 686     }
 687 
 688     private final MenuItem undoMI   = new ContextMenuItem("Undo", e -> undo());
 689     private final MenuItem redoMI   = new ContextMenuItem("Redo", e -> redo());
 690     private final MenuItem cutMI    = new ContextMenuItem("Cut", e -> cut());
 691     private final MenuItem copyMI   = new ContextMenuItem("Copy", e -> getNode().copy());
 692     private final MenuItem pasteMI  = new ContextMenuItem("Paste", e -> paste());
 693     private final MenuItem deleteMI = new ContextMenuItem("DeleteSelection", e -> deleteSelection());
 694     private final MenuItem selectWordMI = new ContextMenuItem("SelectWord", e -> selectNextWord());
 695     private final MenuItem selectAllMI = new ContextMenuItem("SelectAll", e -> getNode().selectAll());
 696     private final MenuItem separatorMI = new SeparatorMenuItem();
 697 
 698 }