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.behavior;
  27 
  28 import javafx.application.ConditionalFeature;
  29 import javafx.beans.InvalidationListener;
  30 import javafx.geometry.NodeOrientation;
  31 import javafx.scene.control.IndexRange;
  32 import javafx.scene.control.TextInputControl;
  33 import javafx.scene.input.KeyEvent;
  34 
  35 import java.text.Bidi;
  36 import java.text.BreakIterator;
  37 import java.util.ArrayList;
  38 import java.util.List;
  39 
  40 import com.sun.javafx.application.PlatformImpl;
  41 import com.sun.javafx.scene.control.skin.TextInputControlSkin;
  42 
  43 import static javafx.scene.input.KeyEvent.KEY_PRESSED;
  44 
  45 import static com.sun.javafx.PlatformUtil.*;
  46 
  47 /**
  48  * Abstract base class for text input behaviors.
  49  */
  50 public abstract class TextInputControlBehavior<T extends TextInputControl> extends BehaviorBase<T> {
  51     /**************************************************************************
  52      *                          Setup KeyBindings                             *
  53      *************************************************************************/
  54     protected static final List<KeyBinding> TEXT_INPUT_BINDINGS = new ArrayList<KeyBinding>();
  55     static {
  56         TEXT_INPUT_BINDINGS.addAll(TextInputControlBindings.BINDINGS);
  57         // However, we want to consume other key press / release events too, for
  58         // things that would have been handled by the InputCharacter normally
  59         TEXT_INPUT_BINDINGS.add(new KeyBinding(null, KEY_PRESSED, "Consume"));
  60     }
  61 
  62     /**************************************************************************
  63      * Fields                                                                 *
  64      *************************************************************************/
  65 
  66     T textInputControl;
  67 
  68     /**
  69      * Used to keep track of the most recent key event. This is used when
  70      * handling InputCharacter actions.
  71      */
  72     private KeyEvent lastEvent;
  73 
  74     private InvalidationListener textListener = observable -> {
  75         invalidateBidi();
  76     };
  77 
  78     /**************************************************************************
  79      * Constructors                                                           *
  80      *************************************************************************/
  81 
  82     /**
  83      * Create a new TextInputControlBehavior.
  84      * @param textInputControl cannot be null
  85      */
  86     public TextInputControlBehavior(T textInputControl, List<KeyBinding> bindings) {
  87         super(textInputControl, bindings);
  88 
  89         this.textInputControl = textInputControl;
  90 
  91         textInputControl.textProperty().addListener(textListener);
  92     }
  93 
  94     /**************************************************************************
  95      * Disposal methods                                                       *
  96      *************************************************************************/
  97 
  98     @Override public void dispose() {
  99         textInputControl.textProperty().removeListener(textListener);
 100         super.dispose();
 101     }
 102 
 103     /**************************************************************************
 104      * Abstract methods                                                       *
 105      *************************************************************************/
 106 
 107     protected abstract void deleteChar(boolean previous);
 108     protected abstract void replaceText(int start, int end, String txt);
 109     protected abstract void setCaretAnimating(boolean play);
 110     protected abstract void deleteFromLineStart();
 111 
 112     protected void scrollCharacterToVisible(int index) {
 113         // TODO this method should be removed when TextAreaSkin
 114         // TODO is refactored to no longer need it.
 115     }
 116 
 117     /**************************************************************************
 118      * Key handling implementation                                            *
 119      *************************************************************************/
 120 
 121     /**
 122      * Records the last KeyEvent we saw.
 123      * @param e
 124      */
 125     @Override protected void callActionForEvent(KeyEvent e) {
 126         lastEvent = e;
 127         super.callActionForEvent(e);
 128     }
 129 
 130     @Override public void callAction(String name) {
 131         TextInputControl textInputControl = getControl();
 132         boolean done = false;
 133 
 134         setCaretAnimating(false);
 135 
 136         if (textInputControl.isEditable()) {
 137             setEditing(true);
 138             done = true;
 139             if ("InputCharacter".equals(name)) defaultKeyTyped(lastEvent);
 140             else if ("Cut".equals(name)) cut();
 141             else if ("Paste".equals(name)) paste();
 142             else if ("DeleteFromLineStart".equals(name)) deleteFromLineStart();
 143             else if ("DeletePreviousChar".equals(name)) deletePreviousChar();
 144             else if ("DeleteNextChar".equals(name)) deleteNextChar();
 145             else if ("DeletePreviousWord".equals(name)) deletePreviousWord();
 146             else if ("DeleteNextWord".equals(name)) deleteNextWord();
 147             else if ("DeleteSelection".equals(name)) deleteSelection();
 148             else if ("Undo".equals(name)) textInputControl.undo();
 149             else if ("Redo".equals(name)) textInputControl.redo();
 150             else {
 151                 done = false;
 152             }
 153             setEditing(false);
 154         }
 155         if (!done) {
 156             done = true;
 157             if ("Copy".equals(name)) textInputControl.copy();
 158             else if ("SelectBackward".equals(name)) textInputControl.selectBackward();
 159             else if ("SelectForward".equals(name)) textInputControl.selectForward();
 160             else if ("SelectLeft".equals(name)) selectLeft();
 161             else if ("SelectRight".equals(name)) selectRight();
 162             else if ("PreviousWord".equals(name)) previousWord();
 163             else if ("NextWord".equals(name)) nextWord();
 164             else if ("LeftWord".equals(name)) leftWord();
 165             else if ("RightWord".equals(name)) rightWord();
 166             else if ("SelectPreviousWord".equals(name)) selectPreviousWord();
 167             else if ("SelectNextWord".equals(name)) selectNextWord();
 168             else if ("SelectLeftWord".equals(name)) selectLeftWord();
 169             else if ("SelectRightWord".equals(name)) selectRightWord();
 170             else if ("SelectWord".equals(name)) selectWord();
 171             else if ("SelectAll".equals(name)) textInputControl.selectAll();
 172             else if ("Home".equals(name)) textInputControl.home();
 173             else if ("End".equals(name)) textInputControl.end();
 174             else if ("Forward".equals(name)) textInputControl.forward();
 175             else if ("Backward".equals(name)) textInputControl.backward();
 176             else if ("Right".equals(name)) nextCharacterVisually(true);
 177             else if ("Left".equals(name)) nextCharacterVisually(false);
 178             else if ("Fire".equals(name)) fire(lastEvent);
 179             else if ("Cancel".equals(name)) cancelEdit(lastEvent);
 180             else if ("Unselect".equals(name)) textInputControl.deselect();
 181             else if ("SelectHome".equals(name)) selectHome();
 182             else if ("SelectEnd".equals(name)) selectEnd();
 183             else if ("SelectHomeExtend".equals(name)) selectHomeExtend();
 184             else if ("SelectEndExtend".equals(name)) selectEndExtend();
 185             else if ("ToParent".equals(name)) forwardToParent(lastEvent);
 186             /*DEBUG*/else if ("UseVK".equals(name) && PlatformImpl.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD)) {
 187                 ((TextInputControlSkin<?,?>)textInputControl.getSkin()).toggleUseVK();
 188             } else {
 189                 done = false;
 190             }
 191         }
 192         setCaretAnimating(true);
 193 
 194         if (!done) {
 195             if ("TraverseNext".equals(name)) traverseNext();
 196             else if ("TraversePrevious".equals(name)) traversePrevious();
 197             else super.callAction(name);
 198 
 199         }
 200         // Note, I don't have to worry about "Consume" here.
 201     }
 202 
 203     /**
 204      * The default handler for a key typed event, which is called when none of
 205      * the other key bindings match. This is the method which handles basic
 206      * text entry.
 207      * @param event not null
 208      */
 209     private void defaultKeyTyped(KeyEvent event) {
 210         final TextInputControl textInput = getControl();
 211         // I'm not sure this case can actually ever happen, maybe this
 212         // should be an assert instead?
 213         if (!textInput.isEditable() || textInput.isDisabled()) return;
 214 
 215         // Sometimes we get events with no key character, in which case
 216         // we need to bail.
 217         String character = event.getCharacter();
 218         if (character.length() == 0) return;
 219 
 220         // Filter out control keys except control+Alt on PC or Alt on Mac
 221         if (event.isControlDown() || event.isAltDown() || (isMac() && event.isMetaDown())) {
 222             if (!((event.isControlDown() || isMac()) && event.isAltDown())) return;
 223         }
 224 
 225         // Ignore characters in the control range and the ASCII delete
 226         // character as well as meta key presses
 227         if (character.charAt(0) > 0x1F
 228             && character.charAt(0) != 0x7F
 229             && !event.isMetaDown()) { // Not sure about this one
 230             final IndexRange selection = textInput.getSelection();
 231             final int start = selection.getStart();
 232             final int end = selection.getEnd();
 233 
 234 //            if (textInput.getLength() - selection.getLength()
 235 //                + character.length() > textInput.getMaximumLength()) {
 236 //                // TODO Beep?
 237 //            } else {
 238                 replaceText(start, end, character);
 239 //            }
 240 
 241             scrollCharacterToVisible(start);
 242         }
 243     }
 244 
 245     private Bidi bidi = null;
 246     private Boolean mixed = null;
 247     private Boolean rtlText = null;
 248 
 249     private void invalidateBidi() {
 250         bidi = null;
 251         mixed = null;
 252         rtlText = null;
 253     }
 254 
 255     private Bidi getBidi() {
 256         if (bidi == null) {
 257             bidi = new Bidi(textInputControl.textProperty().getValueSafe(),
 258                             (textInputControl.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT)
 259                                     ? Bidi.DIRECTION_RIGHT_TO_LEFT
 260                                     : Bidi.DIRECTION_LEFT_TO_RIGHT);
 261         }
 262         return bidi;
 263     }
 264 
 265     protected boolean isMixed() {
 266         if (mixed == null) {
 267             mixed = getBidi().isMixed();
 268         }
 269         return mixed;
 270     }
 271 
 272     protected boolean isRTLText() {
 273         if (rtlText == null) {
 274             Bidi bidi = getBidi();
 275             rtlText =
 276                 (bidi.isRightToLeft() ||
 277                  (isMixed() &&
 278                   textInputControl.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT));
 279         }
 280         return rtlText;
 281     }
 282 
 283     private void nextCharacterVisually(boolean moveRight) {
 284         if (isMixed()) {
 285             TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>)textInputControl.getSkin();
 286             skin.nextCharacterVisually(moveRight);
 287         } else if (moveRight != isRTLText()) {
 288             textInputControl.forward();
 289         } else {
 290             textInputControl.backward();
 291         }
 292     }
 293 
 294     private void selectLeft() {
 295         if (isRTLText()) {
 296             textInputControl.selectForward();
 297         } else {
 298             textInputControl.selectBackward();
 299         }
 300     }
 301 
 302     private void selectRight() {
 303         if (isRTLText()) {
 304             textInputControl.selectBackward();
 305         } else {
 306             textInputControl.selectForward();
 307         }
 308     }
 309 
 310     private void deletePreviousChar() {
 311         deleteChar(true);
 312     }
 313 
 314     private void deleteNextChar() {
 315         deleteChar(false);
 316     }
 317 
 318     protected void deletePreviousWord() {
 319         TextInputControl textInputControl = getControl();
 320         int end = textInputControl.getCaretPosition();
 321 
 322         if (end > 0) {
 323             textInputControl.previousWord();
 324             int start = textInputControl.getCaretPosition();
 325             replaceText(start, end, "");
 326         }
 327     }
 328 
 329     protected void deleteNextWord() {
 330         TextInputControl textInputControl = getControl();
 331         int start = textInputControl.getCaretPosition();
 332 
 333         if (start < textInputControl.getLength()) {
 334             nextWord();
 335             int end = textInputControl.getCaretPosition();
 336             replaceText(start, end, "");
 337         }
 338     }
 339 
 340     private void deleteSelection() {
 341         TextInputControl textInputControl = getControl();
 342         IndexRange selection = textInputControl.getSelection();
 343 
 344         if (selection.getLength() > 0) {
 345             deleteChar(false);
 346         }
 347     }
 348 
 349     private void cut() {
 350         TextInputControl textInputControl = getControl();
 351         textInputControl.cut();
 352     }
 353 
 354     private void paste() {
 355         TextInputControl textInputControl = getControl();
 356         textInputControl.paste();
 357     }
 358 
 359     protected void selectPreviousWord() {
 360         getControl().selectPreviousWord();
 361     }
 362 
 363     protected void selectNextWord() {
 364         TextInputControl textInputControl = getControl();
 365         if (isMac() || isLinux()) {
 366             textInputControl.selectEndOfNextWord();
 367         } else {
 368             textInputControl.selectNextWord();
 369         }
 370     }
 371 
 372     private void selectLeftWord() {
 373         if (isRTLText()) {
 374             selectNextWord();
 375         } else {
 376             selectPreviousWord();
 377         }
 378     }
 379 
 380     private void selectRightWord() {
 381         if (isRTLText()) {
 382             selectPreviousWord();
 383         } else {
 384             selectNextWord();
 385         }
 386     }
 387 
 388     protected void selectWord() {
 389         final TextInputControl textInputControl = getControl();
 390         textInputControl.previousWord();
 391         if (isWindows()) {
 392             textInputControl.selectNextWord();
 393         } else {
 394             textInputControl.selectEndOfNextWord();
 395         }
 396     }
 397 
 398     protected void previousWord() {
 399         getControl().previousWord();
 400     }
 401 
 402     protected void nextWord() {
 403         TextInputControl textInputControl = getControl();
 404         if (isMac() || isLinux()) {
 405             textInputControl.endOfNextWord();
 406         } else {
 407             textInputControl.nextWord();
 408         }
 409     }
 410 
 411     private void leftWord() {
 412         if (isRTLText()) {
 413             nextWord();
 414         } else {
 415             previousWord();
 416         }
 417     }
 418 
 419     private void rightWord() {
 420         if (isRTLText()) {
 421             previousWord();
 422         } else {
 423             nextWord();
 424         }
 425     }
 426 
 427     protected void fire(KeyEvent event) { } // TODO move to TextFieldBehavior
 428     protected void cancelEdit(KeyEvent event) { forwardToParent(event);}
 429 
 430     protected void forwardToParent(KeyEvent event) {
 431         if (getControl().getParent() != null) {
 432             getControl().getParent().fireEvent(event);
 433         }
 434     }
 435 
 436     private void selectHome() {
 437         getControl().selectHome();
 438     }
 439 
 440     private void selectEnd() {
 441         getControl().selectEnd();
 442     }
 443 
 444     private void selectHomeExtend() {
 445         getControl().extendSelection(0);
 446     }
 447 
 448     private void selectEndExtend() {
 449         TextInputControl textInputControl = getControl();
 450         textInputControl.extendSelection(textInputControl.getLength());
 451     }
 452 
 453     private boolean editing = false;
 454     protected void setEditing(boolean b) {
 455         editing = b;
 456     }
 457     public boolean isEditing() {
 458         return editing;
 459     }
 460 }