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