1 /*
   2  * Copyright (c) 2011, 2014, 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 }