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