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