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 }