1 /* 2 * Copyright (c) 2011, 2016, 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 com.sun.javafx.scene.control.Properties; 29 import javafx.beans.value.ChangeListener; 30 import javafx.beans.value.WeakChangeListener; 31 import javafx.event.ActionEvent; 32 import javafx.event.EventHandler; 33 import javafx.geometry.Bounds; 34 import javafx.geometry.Point2D; 35 import javafx.geometry.Rectangle2D; 36 import javafx.scene.Node; 37 import javafx.scene.Scene; 38 import javafx.scene.control.ContextMenu; 39 import javafx.scene.control.TextField; 40 import javafx.scene.control.skin.TextFieldSkin; 41 import com.sun.javafx.scene.control.skin.Utils; 42 import javafx.scene.input.ContextMenuEvent; 43 import javafx.scene.input.KeyEvent; 44 import javafx.scene.input.MouseEvent; 45 import javafx.stage.Screen; 46 import javafx.stage.Window; 47 import com.sun.javafx.PlatformUtil; 48 import com.sun.javafx.geom.transform.Affine3D; 49 50 import static javafx.scene.control.skin.TextFieldSkin.TextPosInfo; 51 import static com.sun.javafx.PlatformUtil.isMac; 52 import static com.sun.javafx.PlatformUtil.isWindows; 53 54 /** 55 * Text field behavior. 56 */ 57 public class TextFieldBehavior extends TextInputControlBehavior<TextField> { 58 private TextFieldSkin skin; 59 private TwoLevelFocusBehavior tlFocus; 60 private ChangeListener<Scene> sceneListener; 61 private ChangeListener<Node> focusOwnerListener; 62 63 public TextFieldBehavior(final TextField textField) { 64 super(textField); 65 66 if (Properties.IS_TOUCH_SUPPORTED) { 67 contextMenu.getStyleClass().add("text-input-context-menu"); 68 } 69 70 handleFocusChange(); 71 72 // Register for change events 73 textField.focusedProperty().addListener((observable, oldValue, newValue) -> { 74 handleFocusChange(); 75 }); 76 77 focusOwnerListener = (observable, oldValue, newValue) -> { 78 // RT-23699: The selection is now only affected when the TextField 79 // gains or loses focus within the Scene, and not when the whole 80 // stage becomes active or inactive. 81 if (newValue == textField) { 82 if (!focusGainedByMouseClick) { 83 textField.selectRange(textField.getLength(), 0); 84 } 85 } else { 86 textField.selectRange(0, 0); 87 } 88 }; 89 90 final WeakChangeListener<Node> weakFocusOwnerListener = 91 new WeakChangeListener<Node>(focusOwnerListener); 92 sceneListener = (observable, oldValue, newValue) -> { 93 if (oldValue != null) { 94 oldValue.focusOwnerProperty().removeListener(weakFocusOwnerListener); 95 } 96 if (newValue != null) { 97 newValue.focusOwnerProperty().addListener(weakFocusOwnerListener); 98 } 99 }; 100 textField.sceneProperty().addListener(new WeakChangeListener<Scene>(sceneListener)); 101 102 if (textField.getScene() != null) { 103 textField.getScene().focusOwnerProperty().addListener(weakFocusOwnerListener); 104 } 105 106 // Only add this if we're on an embedded platform that supports 5-button navigation 107 if (Utils.isTwoLevelFocus()) { 108 tlFocus = new TwoLevelFocusBehavior(textField); // needs to be last. 109 } 110 } 111 112 @Override public void dispose() { 113 if (tlFocus != null) tlFocus.dispose(); 114 super.dispose(); 115 } 116 117 private void handleFocusChange() { 118 TextField textField = getNode(); 119 120 if (textField.isFocused()) { 121 if (PlatformUtil.isIOS()) { 122 // special handling of focus on iOS is required to allow to 123 // control native keyboard, because nat. keyboard is poped-up only when native 124 // text component gets focus. When we have JFX keyboard we can remove this code 125 TextInputTypes type = TextInputTypes.TEXT_FIELD; 126 if (textField.getClass().equals(javafx.scene.control.PasswordField.class)) { 127 type = TextInputTypes.PASSWORD_FIELD; 128 } else if (textField.getParent().getClass().equals(javafx.scene.control.ComboBox.class)) { 129 type = TextInputTypes.EDITABLE_COMBO; 130 } 131 final Bounds bounds = textField.getBoundsInParent(); 132 double w = bounds.getWidth(); 133 double h = bounds.getHeight(); 134 Affine3D trans = calculateNodeToSceneTransform(textField); 135 // Insets insets = skin.getInsets(); 136 // w -= insets.getLeft() + insets.getRight(); 137 // h -= insets.getTop() + insets.getBottom(); 138 String text = textField.getText(); 139 140 // we need to display native text input component on the place where JFX component is drawn 141 // all parameters needed to do that are passed to native impl. here 142 textField.getScene().getWindow().impl_getPeer().requestInput(text, type.ordinal(), w, h, 143 trans.getMxx(), trans.getMxy(), trans.getMxz(), trans.getMxt(),// + insets.getLeft(), 144 trans.getMyx(), trans.getMyy(), trans.getMyz(), trans.getMyt(),// + insets.getTop(), 145 trans.getMzx(), trans.getMzy(), trans.getMzz(), trans.getMzt()); 146 } 147 if (!focusGainedByMouseClick) { 148 setCaretAnimating(true); 149 } 150 } else { 151 if (PlatformUtil.isIOS() && textField.getScene() != null) { 152 // releasing the focus => we need to hide the native component and also native keyboard 153 textField.getScene().getWindow().impl_getPeer().releaseInput(); 154 } 155 focusGainedByMouseClick = false; 156 setCaretAnimating(false); 157 } 158 } 159 160 static Affine3D calculateNodeToSceneTransform(Node node) { 161 final Affine3D transform = new Affine3D(); 162 do { 163 transform.preConcatenate(node.impl_getLeafTransform()); 164 node = node.getParent(); 165 } while (node != null); 166 167 return transform; 168 } 169 170 // An unholy back-reference! 171 public void setTextFieldSkin(TextFieldSkin skin) { 172 this.skin = skin; 173 } 174 175 @Override protected void fire(KeyEvent event) { 176 TextField textField = getNode(); 177 EventHandler<ActionEvent> onAction = textField.getOnAction(); 178 ActionEvent actionEvent = new ActionEvent(textField, null); 179 180 textField.fireEvent(actionEvent); 181 textField.commitValue(); 182 183 if (onAction == null && !actionEvent.isConsumed()) { 184 forwardToParent(event); 185 } 186 } 187 188 @Override 189 protected void cancelEdit(KeyEvent event) { 190 TextField textField = getNode(); 191 if (textField.getTextFormatter() != null) { 192 textField.cancelEdit(); 193 event.consume(); 194 } else { 195 super.cancelEdit(event); 196 } 197 } 198 199 @Override protected void deleteChar(boolean previous) { 200 skin.deleteChar(previous); 201 } 202 203 @Override protected void replaceText(int start, int end, String txt) { 204 skin.setForwardBias(true); 205 skin.replaceText(start, end, txt); 206 } 207 208 @Override protected void deleteFromLineStart() { 209 TextField textField = getNode(); 210 int end = textField.getCaretPosition(); 211 212 if (end > 0) { 213 replaceText(0, end, ""); 214 } 215 } 216 217 @Override protected void setCaretAnimating(boolean play) { 218 if (skin != null) { 219 skin.setCaretAnimating(play); 220 } 221 } 222 223 /** 224 * Function which beeps. This requires a hook into the toolkit, and should 225 * also be guarded by something that indicates whether we should beep 226 * (as it is pretty annoying and many native controls don't do it). 227 */ 228 private void beep() { 229 // TODO 230 } 231 232 /** 233 * If the focus is gained via response to a mouse click, then we don't 234 * want to select all the text even if selectOnFocus is true. 235 */ 236 private boolean focusGainedByMouseClick = false; 237 private boolean shiftDown = false; 238 private boolean deferClick = false; 239 240 @Override public void mousePressed(MouseEvent e) { 241 TextField textField = getNode(); 242 // We never respond to events if disabled 243 if (!textField.isDisabled()) { 244 // If the text field doesn't have focus, then we'll attempt to set 245 // the focus and we'll indicate that we gained focus by a mouse 246 // click, which will then NOT honor the selectOnFocus variable 247 // of the textInputControl 248 if (!textField.isFocused()) { 249 focusGainedByMouseClick = true; 250 textField.requestFocus(); 251 } 252 253 // stop the caret animation 254 setCaretAnimating(false); 255 // only if there is no selection should we see the caret 256 // setCaretOpacity(if (textInputControl.dot == textInputControl.mark) then 1.0 else 0.0); 257 258 // if the primary button was pressed 259 if (e.isPrimaryButtonDown() && !(e.isMiddleButtonDown() || e.isSecondaryButtonDown())) { 260 TextPosInfo hit = skin.getIndex(e.getX(), e.getY()); 261 String text = textField.textProperty().getValueSafe(); 262 int i = Utils.getHitInsertionIndex(hit, text); 263 final int anchor = textField.getAnchor(); 264 final int caretPosition = textField.getCaretPosition(); 265 if (e.getClickCount() < 2 && 266 (Properties.IS_TOUCH_SUPPORTED || 267 (anchor != caretPosition && 268 ((i > anchor && i < caretPosition) || (i < anchor && i > caretPosition))))) { 269 // if there is a selection, then we will NOT handle the 270 // press now, but will defer until the release. If you 271 // select some text and then press down, we change the 272 // caret and wait to allow you to drag the text (TODO). 273 // When the drag concludes, then we handle the click 274 275 deferClick = true; 276 // TODO start a timer such that after some millis we 277 // switch into text dragging mode, change the cursor 278 // to indicate the text can be dragged, etc. 279 } else if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) { 280 switch (e.getClickCount()) { 281 case 1: mouseSingleClick(hit); break; 282 case 2: mouseDoubleClick(hit); break; 283 case 3: mouseTripleClick(hit); break; 284 default: // no-op 285 } 286 } else if (e.isShiftDown() && !(e.isControlDown() || e.isAltDown() || e.isMetaDown()) && e.getClickCount() == 1) { 287 // didn't click inside the selection, so select 288 shiftDown = true; 289 // if we are on mac os, then we will accumulate the 290 // selection instead of just moving the dot. This happens 291 // by figuring out past which (dot/mark) are extending the 292 // selection, and set the mark to be the other side and 293 // the dot to be the new position. 294 // everywhere else we just move the dot. 295 if (isMac()) { 296 textField.extendSelection(i); 297 } else { 298 skin.positionCaret(hit, true); 299 } 300 } 301 skin.setForwardBias(hit.isLeading()); 302 // if (textInputControl.editable) 303 // displaySoftwareKeyboard(true); 304 } 305 } 306 if (contextMenu.isShowing()) { 307 contextMenu.hide(); 308 } 309 } 310 311 @Override public void mouseDragged(MouseEvent e) { 312 final TextField textField = getNode(); 313 // we never respond to events if disabled, but we do notify any onXXX 314 // event listeners on the control 315 if (!textField.isDisabled() && !deferClick) { 316 if (e.isPrimaryButtonDown() && !(e.isMiddleButtonDown() || e.isSecondaryButtonDown())) { 317 if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) { 318 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), true); 319 } 320 } 321 } 322 } 323 324 @Override public void mouseReleased(MouseEvent e) { 325 final TextField textField = getNode(); 326 // we never respond to events if disabled, but we do notify any onXXX 327 // event listeners on the control 328 if (!textField.isDisabled()) { 329 setCaretAnimating(false); 330 if (deferClick) { 331 deferClick = false; 332 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), shiftDown); 333 shiftDown = false; 334 } 335 setCaretAnimating(true); 336 } 337 } 338 339 @Override public void contextMenuRequested(ContextMenuEvent e) { 340 final TextField textField = getNode(); 341 342 if (contextMenu.isShowing()) { 343 contextMenu.hide(); 344 } else if (textField.getContextMenu() == null && 345 textField.getOnContextMenuRequested() == null) { 346 double screenX = e.getScreenX(); 347 double screenY = e.getScreenY(); 348 double sceneX = e.getSceneX(); 349 350 if (Properties.IS_TOUCH_SUPPORTED) { 351 Point2D menuPos; 352 if (textField.getSelection().getLength() == 0) { 353 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false); 354 menuPos = skin.getMenuPosition(); 355 } else { 356 menuPos = skin.getMenuPosition(); 357 if (menuPos != null && (menuPos.getX() <= 0 || menuPos.getY() <= 0)) { 358 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false); 359 menuPos = skin.getMenuPosition(); 360 } 361 } 362 363 if (menuPos != null) { 364 Point2D p = getNode().localToScene(menuPos); 365 Scene scene = getNode().getScene(); 366 Window window = scene.getWindow(); 367 Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(), 368 window.getY() + scene.getY() + p.getY()); 369 screenX = location.getX(); 370 sceneX = p.getX(); 371 screenY = location.getY(); 372 } 373 } 374 375 populateContextMenu(); 376 double menuWidth = contextMenu.prefWidth(-1); 377 double menuX = screenX - (Properties.IS_TOUCH_SUPPORTED ? (menuWidth / 2) : 0); 378 Screen currentScreen = com.sun.javafx.util.Utils.getScreenForPoint(screenX, 0); 379 Rectangle2D bounds = currentScreen.getBounds(); 380 381 if (menuX < bounds.getMinX()) { 382 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX); 383 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX); 384 contextMenu.show(getNode(), bounds.getMinX(), screenY); 385 } else if (screenX + menuWidth > bounds.getMaxX()) { 386 double leftOver = menuWidth - ( bounds.getMaxX() - screenX); 387 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX); 388 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX); 389 contextMenu.show(getNode(), screenX - leftOver, screenY); 390 } else { 391 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", 0); 392 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", 0); 393 contextMenu.show(getNode(), menuX, screenY); 394 } 395 } 396 397 e.consume(); 398 } 399 400 protected void mouseSingleClick(TextPosInfo hit) { 401 skin.positionCaret(hit, false); 402 } 403 404 protected void mouseDoubleClick(TextPosInfo hit) { 405 final TextField textField = getNode(); 406 textField.previousWord(); 407 if (isWindows()) { 408 textField.selectNextWord(); 409 } else { 410 textField.selectEndOfNextWord(); 411 } 412 } 413 414 protected void mouseTripleClick(TextPosInfo hit) { 415 getNode().selectAll(); 416 } 417 418 // Enumeration of all types of text input that can be simulated on 419 // touch device, such as iPad. Type is passed to native code and 420 // native text component is shown. It's used as workaround for iOS 421 // devices since keyboard control is not possible without native 422 // text component being displayed 423 enum TextInputTypes { 424 TEXT_FIELD, 425 PASSWORD_FIELD, 426 EDITABLE_COMBO, 427 TEXT_AREA; 428 } 429 430 }