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