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 }