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.scene.text.HitInfo; 46 import javafx.stage.Screen; 47 import javafx.stage.Window; 48 import com.sun.javafx.PlatformUtil; 49 import com.sun.javafx.geom.transform.Affine3D; 50 51 import static com.sun.javafx.PlatformUtil.isMac; 52 import static com.sun.javafx.PlatformUtil.isWindows; 53 import com.sun.javafx.scene.NodeHelper; 54 import com.sun.javafx.stage.WindowHelper; 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 ActionEvent actionEvent = new ActionEvent(textField, null); 182 183 textField.fireEvent(actionEvent); 184 textField.commitValue(); 185 186 if (onAction == null && !actionEvent.isConsumed()) { 187 forwardToParent(event); 188 } 189 } 190 191 @Override 192 protected void cancelEdit(KeyEvent event) { 193 TextField textField = getNode(); 194 if (textField.getTextFormatter() != null) { 195 textField.cancelEdit(); 196 event.consume(); 197 } else { 198 super.cancelEdit(event); 199 } 200 } 201 202 @Override protected void deleteChar(boolean previous) { 203 skin.deleteChar(previous); 204 } 205 206 @Override protected void replaceText(int start, int end, String txt) { 207 skin.setForwardBias(true); 208 skin.replaceText(start, end, txt); 209 } 210 211 @Override protected void deleteFromLineStart() { 212 TextField textField = getNode(); 213 int end = textField.getCaretPosition(); 214 215 if (end > 0) { 216 replaceText(0, end, ""); 217 } 218 } 219 220 @Override protected void setCaretAnimating(boolean play) { 221 if (skin != null) { 222 skin.setCaretAnimating(play); 223 } 224 } 225 226 /** 227 * Function which beeps. This requires a hook into the toolkit, and should 228 * also be guarded by something that indicates whether we should beep 229 * (as it is pretty annoying and many native controls don't do it). 230 */ 231 private void beep() { 232 // TODO 233 } 234 235 /** 236 * If the focus is gained via response to a mouse click, then we don't 237 * want to select all the text even if selectOnFocus is true. 238 */ 239 private boolean focusGainedByMouseClick = false; 240 private boolean shiftDown = false; 241 private boolean deferClick = false; 242 243 @Override public void mousePressed(MouseEvent e) { 244 TextField textField = getNode(); 245 // We never respond to events if disabled 246 if (!textField.isDisabled()) { 247 // If the text field doesn't have focus, then we'll attempt to set 248 // the focus and we'll indicate that we gained focus by a mouse 249 // click, which will then NOT honor the selectOnFocus variable 250 // of the textInputControl 251 if (!textField.isFocused()) { 252 focusGainedByMouseClick = true; 253 textField.requestFocus(); 254 } 255 256 // stop the caret animation 257 setCaretAnimating(false); 258 // only if there is no selection should we see the caret 259 // setCaretOpacity(if (textInputControl.dot == textInputControl.mark) then 1.0 else 0.0); 260 261 // if the primary button was pressed 262 if (e.isPrimaryButtonDown() && !(e.isMiddleButtonDown() || e.isSecondaryButtonDown())) { 263 HitInfo hit = skin.getIndex(e.getX(), e.getY()); 264 int i = hit.getInsertionIndex(); 265 final int anchor = textField.getAnchor(); 266 final int caretPosition = textField.getCaretPosition(); 267 if (e.getClickCount() < 2 && 268 (Properties.IS_TOUCH_SUPPORTED || 269 (anchor != caretPosition && 270 ((i > anchor && i < caretPosition) || (i < anchor && i > caretPosition))))) { 271 // if there is a selection, then we will NOT handle the 272 // press now, but will defer until the release. If you 273 // select some text and then press down, we change the 274 // caret and wait to allow you to drag the text (TODO). 275 // When the drag concludes, then we handle the click 276 277 deferClick = true; 278 // TODO start a timer such that after some millis we 279 // switch into text dragging mode, change the cursor 280 // to indicate the text can be dragged, etc. 281 } else if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) { 282 switch (e.getClickCount()) { 283 case 1: mouseSingleClick(hit); break; 284 case 2: mouseDoubleClick(hit); break; 285 case 3: mouseTripleClick(hit); break; 286 default: // no-op 287 } 288 } else if (e.isShiftDown() && !(e.isControlDown() || e.isAltDown() || e.isMetaDown()) && e.getClickCount() == 1) { 289 // didn't click inside the selection, so select 290 shiftDown = true; 291 // if we are on mac os, then we will accumulate the 292 // selection instead of just moving the dot. This happens 293 // by figuring out past which (dot/mark) are extending the 294 // selection, and set the mark to be the other side and 295 // the dot to be the new position. 296 // everywhere else we just move the dot. 297 if (isMac()) { 298 textField.extendSelection(i); 299 } else { 300 skin.positionCaret(hit, true); 301 } 302 } 303 skin.setForwardBias(hit.isLeading()); 304 // if (textInputControl.editable) 305 // displaySoftwareKeyboard(true); 306 } 307 } 308 if (contextMenu.isShowing()) { 309 contextMenu.hide(); 310 } 311 } 312 313 @Override public void mouseDragged(MouseEvent e) { 314 final TextField textField = getNode(); 315 // we never respond to events if disabled, but we do notify any onXXX 316 // event listeners on the control 317 if (!textField.isDisabled() && !deferClick) { 318 if (e.isPrimaryButtonDown() && !(e.isMiddleButtonDown() || e.isSecondaryButtonDown())) { 319 if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) { 320 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), true); 321 } 322 } 323 } 324 } 325 326 @Override public void mouseReleased(MouseEvent e) { 327 final TextField textField = getNode(); 328 // we never respond to events if disabled, but we do notify any onXXX 329 // event listeners on the control 330 if (!textField.isDisabled()) { 331 setCaretAnimating(false); 332 if (deferClick) { 333 deferClick = false; 334 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), shiftDown); 335 shiftDown = false; 336 } 337 setCaretAnimating(true); 338 } 339 } 340 341 @Override public void contextMenuRequested(ContextMenuEvent e) { 342 final TextField textField = getNode(); 343 344 if (contextMenu.isShowing()) { 345 contextMenu.hide(); 346 } else if (textField.getContextMenu() == null && 347 textField.getOnContextMenuRequested() == null) { 348 double screenX = e.getScreenX(); 349 double screenY = e.getScreenY(); 350 double sceneX = e.getSceneX(); 351 352 if (Properties.IS_TOUCH_SUPPORTED) { 353 Point2D menuPos; 354 if (textField.getSelection().getLength() == 0) { 355 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false); 356 menuPos = skin.getMenuPosition(); 357 } else { 358 menuPos = skin.getMenuPosition(); 359 if (menuPos != null && (menuPos.getX() <= 0 || menuPos.getY() <= 0)) { 360 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false); 361 menuPos = skin.getMenuPosition(); 362 } 363 } 364 365 if (menuPos != null) { 366 Point2D p = getNode().localToScene(menuPos); 367 Scene scene = getNode().getScene(); 368 Window window = scene.getWindow(); 369 Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(), 370 window.getY() + scene.getY() + p.getY()); 371 screenX = location.getX(); 372 sceneX = p.getX(); 373 screenY = location.getY(); 374 } 375 } 376 377 populateContextMenu(); 378 double menuWidth = contextMenu.prefWidth(-1); 379 double menuX = screenX - (Properties.IS_TOUCH_SUPPORTED ? (menuWidth / 2) : 0); 380 Screen currentScreen = com.sun.javafx.util.Utils.getScreenForPoint(screenX, 0); 381 Rectangle2D bounds = currentScreen.getBounds(); 382 383 if (menuX < bounds.getMinX()) { 384 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX); 385 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX); 386 contextMenu.show(getNode(), bounds.getMinX(), screenY); 387 } else if (screenX + menuWidth > bounds.getMaxX()) { 388 double leftOver = menuWidth - ( bounds.getMaxX() - screenX); 389 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX); 390 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX); 391 contextMenu.show(getNode(), screenX - leftOver, screenY); 392 } else { 393 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", 0); 394 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", 0); 395 contextMenu.show(getNode(), menuX, screenY); 396 } 397 } 398 399 e.consume(); 400 } 401 402 protected void mouseSingleClick(HitInfo hit) { 403 skin.positionCaret(hit, false); 404 } 405 406 protected void mouseDoubleClick(HitInfo hit) { 407 final TextField textField = getNode(); 408 textField.previousWord(); 409 if (isWindows()) { 410 textField.selectNextWord(); 411 } else { 412 textField.selectEndOfNextWord(); 413 } 414 } 415 416 protected void mouseTripleClick(HitInfo hit) { 417 getNode().selectAll(); 418 } 419 420 // Enumeration of all types of text input that can be simulated on 421 // touch device, such as iPad. Type is passed to native code and 422 // native text component is shown. It's used as workaround for iOS 423 // devices since keyboard control is not possible without native 424 // text component being displayed 425 enum TextInputTypes { 426 TEXT_FIELD, 427 PASSWORD_FIELD, 428 EDITABLE_COMBO, 429 TEXT_AREA; 430 } 431 432 }