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 }