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.application.ConditionalFeature;
29 import javafx.beans.InvalidationListener;
30 import javafx.geometry.NodeOrientation;
31 import javafx.scene.control.IndexRange;
32 import javafx.scene.control.TextInputControl;
33 import javafx.scene.input.KeyEvent;
34
35 import java.text.Bidi;
36 import java.text.BreakIterator;
37 import java.util.ArrayList;
38 import java.util.List;
39
40 import com.sun.javafx.application.PlatformImpl;
41 import com.sun.javafx.scene.control.skin.TextInputControlSkin;
42
43 import static javafx.scene.input.KeyEvent.KEY_PRESSED;
44
45 import static com.sun.javafx.PlatformUtil.*;
46
47 /**
48 * Abstract base class for text input behaviors.
49 */
50 public abstract class TextInputControlBehavior<T extends TextInputControl> extends BehaviorBase<T> {
51 /**************************************************************************
52 * Setup KeyBindings *
53 *************************************************************************/
54 protected static final List<KeyBinding> TEXT_INPUT_BINDINGS = new ArrayList<KeyBinding>();
55 static {
56 TEXT_INPUT_BINDINGS.addAll(TextInputControlBindings.BINDINGS);
57 // However, we want to consume other key press / release events too, for
58 // things that would have been handled by the InputCharacter normally
59 TEXT_INPUT_BINDINGS.add(new KeyBinding(null, KEY_PRESSED, "Consume"));
60 }
61
62 /**************************************************************************
63 * Fields *
64 *************************************************************************/
65
66 T textInputControl;
67
68 /**
69 * Used to keep track of the most recent key event. This is used when
70 * handling InputCharacter actions.
71 */
72 private KeyEvent lastEvent;
73
74 private InvalidationListener textListener = observable -> {
75 invalidateBidi();
76 };
77
78 /**************************************************************************
79 * Constructors *
80 *************************************************************************/
81
82 /**
83 * Create a new TextInputControlBehavior.
84 * @param textInputControl cannot be null
85 */
86 public TextInputControlBehavior(T textInputControl, List<KeyBinding> bindings) {
87 super(textInputControl, bindings);
88
89 this.textInputControl = textInputControl;
90
91 textInputControl.textProperty().addListener(textListener);
92 }
93
94 /**************************************************************************
95 * Disposal methods *
96 *************************************************************************/
97
98 @Override public void dispose() {
99 textInputControl.textProperty().removeListener(textListener);
100 super.dispose();
101 }
102
103 /**************************************************************************
104 * Abstract methods *
105 *************************************************************************/
106
107 protected abstract void deleteChar(boolean previous);
108 protected abstract void replaceText(int start, int end, String txt);
109 protected abstract void setCaretAnimating(boolean play);
110 protected abstract void deleteFromLineStart();
111
112 protected void scrollCharacterToVisible(int index) {
113 // TODO this method should be removed when TextAreaSkin
114 // TODO is refactored to no longer need it.
115 }
116
117 /**************************************************************************
118 * Key handling implementation *
119 *************************************************************************/
120
121 /**
122 * Records the last KeyEvent we saw.
123 * @param e
124 */
125 @Override protected void callActionForEvent(KeyEvent e) {
126 lastEvent = e;
127 super.callActionForEvent(e);
128 }
129
130 @Override public void callAction(String name) {
131 TextInputControl textInputControl = getControl();
132 boolean done = false;
133
134 setCaretAnimating(false);
135
136 if (textInputControl.isEditable()) {
137 setEditing(true);
138 done = true;
139 if ("InputCharacter".equals(name)) defaultKeyTyped(lastEvent);
140 else if ("Cut".equals(name)) cut();
141 else if ("Paste".equals(name)) paste();
142 else if ("DeleteFromLineStart".equals(name)) deleteFromLineStart();
143 else if ("DeletePreviousChar".equals(name)) deletePreviousChar();
144 else if ("DeleteNextChar".equals(name)) deleteNextChar();
145 else if ("DeletePreviousWord".equals(name)) deletePreviousWord();
146 else if ("DeleteNextWord".equals(name)) deleteNextWord();
147 else if ("DeleteSelection".equals(name)) deleteSelection();
148 else if ("Undo".equals(name)) textInputControl.undo();
149 else if ("Redo".equals(name)) textInputControl.redo();
150 else {
151 done = false;
152 }
153 setEditing(false);
154 }
155 if (!done) {
156 done = true;
157 if ("Copy".equals(name)) textInputControl.copy();
158 else if ("SelectBackward".equals(name)) textInputControl.selectBackward();
159 else if ("SelectForward".equals(name)) textInputControl.selectForward();
160 else if ("SelectLeft".equals(name)) selectLeft();
161 else if ("SelectRight".equals(name)) selectRight();
162 else if ("PreviousWord".equals(name)) previousWord();
163 else if ("NextWord".equals(name)) nextWord();
164 else if ("LeftWord".equals(name)) leftWord();
165 else if ("RightWord".equals(name)) rightWord();
166 else if ("SelectPreviousWord".equals(name)) selectPreviousWord();
167 else if ("SelectNextWord".equals(name)) selectNextWord();
168 else if ("SelectLeftWord".equals(name)) selectLeftWord();
169 else if ("SelectRightWord".equals(name)) selectRightWord();
170 else if ("SelectWord".equals(name)) selectWord();
171 else if ("SelectAll".equals(name)) textInputControl.selectAll();
172 else if ("Home".equals(name)) textInputControl.home();
173 else if ("End".equals(name)) textInputControl.end();
174 else if ("Forward".equals(name)) textInputControl.forward();
175 else if ("Backward".equals(name)) textInputControl.backward();
176 else if ("Right".equals(name)) nextCharacterVisually(true);
177 else if ("Left".equals(name)) nextCharacterVisually(false);
178 else if ("Fire".equals(name)) fire(lastEvent);
179 else if ("Cancel".equals(name)) cancelEdit(lastEvent);
180 else if ("Unselect".equals(name)) textInputControl.deselect();
181 else if ("SelectHome".equals(name)) selectHome();
182 else if ("SelectEnd".equals(name)) selectEnd();
183 else if ("SelectHomeExtend".equals(name)) selectHomeExtend();
184 else if ("SelectEndExtend".equals(name)) selectEndExtend();
185 else if ("ToParent".equals(name)) forwardToParent(lastEvent);
186 /*DEBUG*/else if ("UseVK".equals(name) && PlatformImpl.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD)) {
187 ((TextInputControlSkin<?,?>)textInputControl.getSkin()).toggleUseVK();
188 } else {
189 done = false;
190 }
191 }
192 setCaretAnimating(true);
193
194 if (!done) {
195 if ("TraverseNext".equals(name)) traverseNext();
196 else if ("TraversePrevious".equals(name)) traversePrevious();
197 else super.callAction(name);
198
199 }
200 // Note, I don't have to worry about "Consume" here.
201 }
202
203 /**
204 * The default handler for a key typed event, which is called when none of
205 * the other key bindings match. This is the method which handles basic
206 * text entry.
207 * @param event not null
208 */
209 private void defaultKeyTyped(KeyEvent event) {
210 final TextInputControl textInput = getControl();
211 // I'm not sure this case can actually ever happen, maybe this
212 // should be an assert instead?
213 if (!textInput.isEditable() || textInput.isDisabled()) return;
214
215 // Sometimes we get events with no key character, in which case
216 // we need to bail.
217 String character = event.getCharacter();
218 if (character.length() == 0) return;
219
220 // Filter out control keys except control+Alt on PC or Alt on Mac
221 if (event.isControlDown() || event.isAltDown() || (isMac() && event.isMetaDown())) {
222 if (!((event.isControlDown() || isMac()) && event.isAltDown())) return;
223 }
224
225 // Ignore characters in the control range and the ASCII delete
226 // character as well as meta key presses
227 if (character.charAt(0) > 0x1F
228 && character.charAt(0) != 0x7F
229 && !event.isMetaDown()) { // Not sure about this one
230 final IndexRange selection = textInput.getSelection();
231 final int start = selection.getStart();
232 final int end = selection.getEnd();
233
234 // if (textInput.getLength() - selection.getLength()
235 // + character.length() > textInput.getMaximumLength()) {
236 // // TODO Beep?
237 // } else {
238 replaceText(start, end, character);
239 // }
240
241 scrollCharacterToVisible(start);
242 }
243 }
244
245 private Bidi bidi = null;
246 private Boolean mixed = null;
247 private Boolean rtlText = null;
248
249 private void invalidateBidi() {
250 bidi = null;
251 mixed = null;
252 rtlText = null;
253 }
254
255 private Bidi getBidi() {
256 if (bidi == null) {
257 bidi = new Bidi(textInputControl.textProperty().getValueSafe(),
258 (textInputControl.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT)
259 ? Bidi.DIRECTION_RIGHT_TO_LEFT
260 : Bidi.DIRECTION_LEFT_TO_RIGHT);
261 }
262 return bidi;
265 protected boolean isMixed() {
266 if (mixed == null) {
267 mixed = getBidi().isMixed();
268 }
269 return mixed;
270 }
271
272 protected boolean isRTLText() {
273 if (rtlText == null) {
274 Bidi bidi = getBidi();
275 rtlText =
276 (bidi.isRightToLeft() ||
277 (isMixed() &&
278 textInputControl.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT));
279 }
280 return rtlText;
281 }
282
283 private void nextCharacterVisually(boolean moveRight) {
284 if (isMixed()) {
285 TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>)textInputControl.getSkin();
286 skin.nextCharacterVisually(moveRight);
287 } else if (moveRight != isRTLText()) {
288 textInputControl.forward();
289 } else {
290 textInputControl.backward();
291 }
292 }
293
294 private void selectLeft() {
295 if (isRTLText()) {
296 textInputControl.selectForward();
297 } else {
298 textInputControl.selectBackward();
299 }
300 }
301
302 private void selectRight() {
303 if (isRTLText()) {
304 textInputControl.selectBackward();
305 } else {
306 textInputControl.selectForward();
307 }
308 }
309
310 private void deletePreviousChar() {
311 deleteChar(true);
312 }
313
314 private void deleteNextChar() {
315 deleteChar(false);
316 }
317
318 protected void deletePreviousWord() {
319 TextInputControl textInputControl = getControl();
320 int end = textInputControl.getCaretPosition();
321
322 if (end > 0) {
323 textInputControl.previousWord();
324 int start = textInputControl.getCaretPosition();
325 replaceText(start, end, "");
326 }
327 }
328
329 protected void deleteNextWord() {
330 TextInputControl textInputControl = getControl();
331 int start = textInputControl.getCaretPosition();
332
333 if (start < textInputControl.getLength()) {
334 nextWord();
335 int end = textInputControl.getCaretPosition();
336 replaceText(start, end, "");
337 }
338 }
339
340 private void deleteSelection() {
341 TextInputControl textInputControl = getControl();
342 IndexRange selection = textInputControl.getSelection();
343
344 if (selection.getLength() > 0) {
345 deleteChar(false);
346 }
347 }
348
349 private void cut() {
350 TextInputControl textInputControl = getControl();
351 textInputControl.cut();
352 }
353
354 private void paste() {
355 TextInputControl textInputControl = getControl();
356 textInputControl.paste();
357 }
358
359 protected void selectPreviousWord() {
360 getControl().selectPreviousWord();
361 }
362
363 protected void selectNextWord() {
364 TextInputControl textInputControl = getControl();
365 if (isMac() || isLinux()) {
366 textInputControl.selectEndOfNextWord();
367 } else {
368 textInputControl.selectNextWord();
369 }
370 }
371
372 private void selectLeftWord() {
373 if (isRTLText()) {
374 selectNextWord();
375 } else {
376 selectPreviousWord();
377 }
378 }
379
380 private void selectRightWord() {
381 if (isRTLText()) {
382 selectPreviousWord();
383 } else {
384 selectNextWord();
385 }
386 }
387
388 protected void selectWord() {
389 final TextInputControl textInputControl = getControl();
390 textInputControl.previousWord();
391 if (isWindows()) {
392 textInputControl.selectNextWord();
393 } else {
394 textInputControl.selectEndOfNextWord();
395 }
396 }
397
398 protected void previousWord() {
399 getControl().previousWord();
400 }
401
402 protected void nextWord() {
403 TextInputControl textInputControl = getControl();
404 if (isMac() || isLinux()) {
405 textInputControl.endOfNextWord();
406 } else {
407 textInputControl.nextWord();
408 }
409 }
410
411 private void leftWord() {
412 if (isRTLText()) {
413 nextWord();
414 } else {
415 previousWord();
416 }
417 }
418
419 private void rightWord() {
420 if (isRTLText()) {
421 previousWord();
422 } else {
423 nextWord();
424 }
425 }
426
427 protected void fire(KeyEvent event) { } // TODO move to TextFieldBehavior
428 protected void cancelEdit(KeyEvent event) { forwardToParent(event);}
429
430 protected void forwardToParent(KeyEvent event) {
431 if (getControl().getParent() != null) {
432 getControl().getParent().fireEvent(event);
433 }
434 }
435
436 private void selectHome() {
437 getControl().selectHome();
438 }
439
440 private void selectEnd() {
441 getControl().selectEnd();
442 }
443
444 private void selectHomeExtend() {
445 getControl().extendSelection(0);
446 }
447
448 private void selectEndExtend() {
449 TextInputControl textInputControl = getControl();
450 textInputControl.extendSelection(textInputControl.getLength());
451 }
452
453 private boolean editing = false;
454 protected void setEditing(boolean b) {
455 editing = b;
456 }
457 public boolean isEditing() {
458 return editing;
459 }
460 }
|
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 package com.sun.javafx.scene.control.behavior;
26
27 import com.sun.javafx.PlatformUtil;
28 import com.sun.javafx.application.PlatformImpl;
29 import com.sun.javafx.scene.control.Properties;
30 import com.sun.javafx.scene.control.skin.FXVK;
31
32 import javafx.event.ActionEvent;
33 import javafx.event.Event;
34 import javafx.event.EventHandler;
35 import javafx.scene.control.skin.TextInputControlSkin;
36 import javafx.application.ConditionalFeature;
37 import javafx.beans.InvalidationListener;
38 import javafx.collections.ObservableList;
39 import javafx.geometry.NodeOrientation;
40 import javafx.scene.control.ContextMenu;
41 import javafx.scene.control.IndexRange;
42 import javafx.scene.control.MenuItem;
43 import javafx.scene.control.PasswordField;
44 import javafx.scene.control.SeparatorMenuItem;
45 import javafx.scene.control.TextInputControl;
46 import javafx.scene.input.ContextMenuEvent;
47 import javafx.scene.input.Clipboard;
48 import com.sun.javafx.scene.control.inputmap.InputMap;
49 import com.sun.javafx.scene.control.inputmap.KeyBinding;
50 import javafx.scene.input.KeyCode;
51 import javafx.scene.input.KeyEvent;
52 import javafx.scene.input.MouseEvent;
53
54 import java.text.Bidi;
55 import java.util.function.Predicate;
56
57 import static com.sun.javafx.PlatformUtil.isLinux;
58 import static com.sun.javafx.PlatformUtil.isMac;
59 import static com.sun.javafx.PlatformUtil.isWindows;
60 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
61 import static javafx.scene.control.skin.TextInputControlSkin.TextUnit;
62 import static javafx.scene.control.skin.TextInputControlSkin.Direction;
63 import static com.sun.javafx.scene.control.inputmap.InputMap.KeyMapping;
64 import static com.sun.javafx.scene.control.inputmap.InputMap.MouseMapping;
65 import static javafx.scene.input.KeyCode.*;
66 import static javafx.scene.input.KeyEvent.*;
67
68 /**
69 * All of the "button" types (CheckBox, RadioButton, ToggleButton, and Button)
70 * and also maybe some other types like hyperlinks operate on the "armed"
71 * selection strategy, just like JButton. This behavior class encapsulates that
72 * logic in a way that can be reused and extended by each of the individual
73 * class behaviors.
74 *
75 */
76 public abstract class TextInputControlBehavior<T extends TextInputControl> extends BehaviorBase<T> {
77
78 /**
79 * Specifies whether we ought to show handles. We should do it on touch platforms, but not
80 * iOS (and maybe not Android either?)
81 */
82 static final boolean SHOW_HANDLES = Properties.IS_TOUCH_SUPPORTED && !PlatformUtil.isIOS();
83
84 /**************************************************************************
85 * Fields *
86 *************************************************************************/
87
88 final T textInputControl;
89
90 /**
91 * Used to keep track of the most recent key event. This is used when
92 * handling InputCharacter actions.
93 */
94 private KeyEvent lastEvent;
95
96 private InvalidationListener textListener = observable -> {
97 invalidateBidi();
98 };
99
100 private final InputMap<T> inputMap;
101
102
103
104
105 /***************************************************************************
106 * *
107 * Constructors *
108 * *
109 **************************************************************************/
110
111 public TextInputControlBehavior(T c) {
112 super(c);
113
114 this.textInputControl = c;
115
116 textInputControl.textProperty().addListener(textListener);
117
118 // create a map for text input-specific mappings (this reuses the default
119 // InputMap installed on the control, if it is non-null, allowing us to pick up any user-specified mappings)
120 inputMap = createInputMap();
121
122 // some of the mappings are only valid when the control is editable, or
123 // only on certain platforms, so we create the following predicates that filters out the mapping when the
124 // control is not in the correct state / on the correct platform
125 final Predicate<KeyEvent> validWhenEditable = e -> !c.isEditable();
126 final Predicate<KeyEvent> validOnMac = e -> !PlatformUtil.isMac();
127 final Predicate<KeyEvent> validOnWindows = e -> !PlatformUtil.isWindows();
128 final Predicate<KeyEvent> validOnLinux = e -> !PlatformUtil.isLinux();
129
130 // create a child input map for mappings which are applicable on all
131 // platforms, and regardless of editing state
132 addDefaultMapping(inputMap,
133 // caret movement
134 keyMapping(RIGHT, e -> nextCharacterVisually(true)),
135 keyMapping(LEFT, e -> nextCharacterVisually(false)),
136 keyMapping(UP, e -> c.home()),
137 keyMapping(HOME, e -> c.home()),
138 keyMapping(DOWN, e -> c.end()),
139 keyMapping(END, e -> c.end()),
140 keyMapping(ENTER, this::fire),
141
142 keyMapping(new KeyBinding(HOME).shortcut(), e -> c.home()),
143 keyMapping(new KeyBinding(END).shortcut(), e -> c.end()),
144
145 // deletion (only applies when control is editable)
146 keyMapping(new KeyBinding(BACK_SPACE), e -> deletePreviousChar(), validWhenEditable),
147 keyMapping(new KeyBinding(BACK_SPACE).shift(), e -> deletePreviousChar(), validWhenEditable),
148 keyMapping(new KeyBinding(DELETE), e -> deleteNextChar(), validWhenEditable),
149 keyMapping(new KeyBinding(DELETE).shift(), e -> deleteNextChar(), validWhenEditable),
150
151 // cut (only applies when control is editable)
152 keyMapping(new KeyBinding(X).shortcut(), e -> c.cut(), validWhenEditable),
153 keyMapping(new KeyBinding(CUT), e -> cut(), validWhenEditable),
154 keyMapping(new KeyBinding(DELETE).shift(), e -> cut(), validWhenEditable),
155
156 // copy
157 keyMapping(new KeyBinding(C).shortcut(), e -> c.copy()),
158 keyMapping(new KeyBinding(INSERT).shortcut(), e -> c.copy()),
159 keyMapping(COPY, e -> c.copy()),
160
161 // paste (only applies when control is editable)
162 keyMapping(new KeyBinding(V).shortcut(), e -> c.paste(), validWhenEditable),
163 keyMapping(new KeyBinding(PASTE), e -> paste(), validWhenEditable),
164 keyMapping(new KeyBinding(INSERT).shift(), e -> paste(), validWhenEditable),
165
166 // selection
167 keyMapping(new KeyBinding(RIGHT).shift(), e -> selectRight()),
168 keyMapping(new KeyBinding(LEFT).shift(), e -> selectLeft()),
169 keyMapping(new KeyBinding(UP).shift(), e -> selectHome()),
170 keyMapping(new KeyBinding(DOWN).shift(), e -> selectEnd()),
171 keyMapping(new KeyBinding(HOME).shortcut().shift(), e -> selectHome()),
172 keyMapping(new KeyBinding(END).shortcut().shift(), e -> selectEnd()),
173 keyMapping(new KeyBinding(LEFT).shortcut().shift(), e -> selectHomeExtend()),
174 keyMapping(new KeyBinding(RIGHT).shortcut().shift(), e -> selectEndExtend()),
175 keyMapping(new KeyBinding(A).shortcut(), e -> c.selectAll()),
176
177 // Traversal Bindings
178 new KeyMapping(new KeyBinding(TAB), FocusTraversalInputMap::traverseNext),
179 new KeyMapping(new KeyBinding(TAB).shift(), FocusTraversalInputMap::traversePrevious),
180 new KeyMapping(new KeyBinding(TAB).ctrl(), FocusTraversalInputMap::traverseNext),
181 new KeyMapping(new KeyBinding(TAB).ctrl().shift(), FocusTraversalInputMap::traversePrevious),
182
183 // The following keys are forwarded to the parent container
184 new KeyMapping(ESCAPE, this::cancelEdit),
185 new KeyMapping(F10, this::forwardToParent),
186
187 // Linux specific mappings
188 keyMapping(new KeyBinding(Z).ctrl(), e -> undo(), validOnLinux),
189 keyMapping(new KeyBinding(Z).ctrl().shift(), e -> redo(), validOnLinux),
190
191 // character input.
192 // Any other key press first goes to normal text input
193 // Note this is KEY_TYPED because otherwise the character is not available in the event.
194 keyMapping(new KeyBinding(null, KEY_TYPED), this::defaultKeyTyped),
195
196 // However, we want to consume other key press / release events too, for
197 // things that would have been handled by the InputCharacter normally
198 keyMapping(new KeyBinding(null, KEY_PRESSED), e -> e.consume()),
199
200 // VK
201 new KeyMapping(new KeyBinding(DIGIT9).ctrl().shift(), e -> {
202 FXVK.toggleUseVK(textInputControl);
203 }, p -> !PlatformImpl.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD)),
204
205 // mouse and context menu mappings
206 new MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed),
207 new MouseMapping(MouseEvent.MOUSE_DRAGGED, this::mouseDragged),
208 new MouseMapping(MouseEvent.MOUSE_RELEASED, this::mouseReleased),
209 new InputMap.Mapping<ContextMenuEvent>(ContextMenuEvent.CONTEXT_MENU_REQUESTED, this::contextMenuRequested) {
210 @Override public int getSpecificity(Event event) {
211 return 1;
212 }
213 }
214 );
215
216 // mac os specific mappings
217 InputMap<T> macOsInputMap = new InputMap<>(c);
218 macOsInputMap.setInterceptor(e -> !PlatformUtil.isMac());
219 macOsInputMap.getMappings().addAll(
220 // Mac OS specific mappings
221 keyMapping(new KeyBinding(HOME).shift(), e -> selectHomeExtend()),
222 keyMapping(new KeyBinding(END).shift(), e -> selectEndExtend()),
223 keyMapping(new KeyBinding(LEFT).shortcut(), e -> c.home()),
224 keyMapping(new KeyBinding(RIGHT).shortcut(), e -> c.end()),
225 keyMapping(new KeyBinding(LEFT).alt(), e -> leftWord()),
226 keyMapping(new KeyBinding(RIGHT).alt(), e -> rightWord()),
227 keyMapping(new KeyBinding(DELETE).alt(), e -> deleteNextWord()),
228 keyMapping(new KeyBinding(BACK_SPACE).alt(), e -> deletePreviousWord()),
229 keyMapping(new KeyBinding(BACK_SPACE).shortcut(), e -> deleteFromLineStart()),
230 keyMapping(new KeyBinding(Z).shortcut(), e -> undo()),
231 keyMapping(new KeyBinding(Z).shortcut().shift(), e -> redo()),
232
233 // Mac OS specific selection mappings
234 keyMapping(new KeyBinding(LEFT).shift().alt(), e -> selectLeftWord()),
235 keyMapping(new KeyBinding(RIGHT).shift().alt(), e -> selectRightWord())
236 );
237 addDefaultChildMap(inputMap, macOsInputMap);
238
239 // windows / linux specific mappings
240 InputMap<T> nonMacOsInputMap = new InputMap<>(c);
241 nonMacOsInputMap.setInterceptor(e -> PlatformUtil.isMac());
242 nonMacOsInputMap.getMappings().addAll(
243 keyMapping(new KeyBinding(HOME).shift(), e -> selectHome()),
244 keyMapping(new KeyBinding(END).shift(), e -> selectEnd()),
245 keyMapping(new KeyBinding(LEFT).ctrl(), e -> leftWord()),
246 keyMapping(new KeyBinding(RIGHT).ctrl(), e -> rightWord()),
247 keyMapping(new KeyBinding(H).ctrl(), e -> deletePreviousChar()),
248 keyMapping(new KeyBinding(DELETE).ctrl(), e -> deleteNextWord()),
249 keyMapping(new KeyBinding(BACK_SPACE).ctrl(), e -> deletePreviousWord()),
250 keyMapping(new KeyBinding(BACK_SLASH).ctrl(), e -> c.deselect()),
251 keyMapping(new KeyBinding(Z).ctrl(), e -> undo()),
252 keyMapping(new KeyBinding(Y).ctrl(), e -> redo())
253 );
254 addDefaultChildMap(inputMap, nonMacOsInputMap);
255
256 addKeyPadMappings(inputMap);
257
258 textInputControl.textProperty().addListener(textListener);
259 }
260
261 @Override public InputMap<T> getInputMap() {
262 return inputMap;
263 }
264
265 /**
266 * Bind keypad arrow keys to the same as the regular arrow keys.
267 */
268 protected void addKeyPadMappings(InputMap<T> map) {
269 // First create a temporary map for the keypad mappings
270 InputMap<T> tmpMap = new InputMap<>(getNode());
271 for (Object o : map.getMappings()) {
272 if (o instanceof KeyMapping) {
273 KeyMapping mapping = (KeyMapping)o;
274 KeyBinding kb = (KeyBinding)mapping.getMappingKey();
275 if (kb.getCode() != null) {
276 KeyCode newCode = null;
277 switch (kb.getCode()) {
278 case LEFT: newCode = KP_LEFT; break;
279 case RIGHT: newCode = KP_RIGHT; break;
280 case UP: newCode = KP_UP; break;
281 case DOWN: newCode = KP_DOWN; break;
282 }
283 if (newCode != null) {
284 KeyBinding newkb = new KeyBinding(newCode).shift(kb.getShift())
285 .ctrl(kb.getCtrl())
286 .alt(kb.getAlt())
287 .meta(kb.getMeta());
288 tmpMap.getMappings().add(new KeyMapping(newkb, mapping.getEventHandler()));
289 }
290 }
291 }
292 }
293 // Install mappings
294 for (Object o : tmpMap.getMappings()) {
295 map.getMappings().add((KeyMapping)o);
296 }
297
298 // Recursive call for child maps
299 for (Object o : map.getChildInputMaps()) {
300 addKeyPadMappings((InputMap<T>)o);
301 }
302 }
303
304
305 /**
306 * Wraps the event handler to pause caret blinking when
307 * processing the key event.
308 */
309 protected KeyMapping keyMapping(final KeyCode keyCode, final EventHandler<KeyEvent> eventHandler) {
310 return keyMapping(new KeyBinding(keyCode), eventHandler);
311 }
312
313 protected KeyMapping keyMapping(KeyBinding keyBinding, final EventHandler<KeyEvent> eventHandler) {
314 return keyMapping(keyBinding, eventHandler, null);
315 }
316
317 protected KeyMapping keyMapping(KeyBinding keyBinding, final EventHandler<KeyEvent> eventHandler,
318 Predicate<KeyEvent> interceptor) {
319 return new KeyMapping(keyBinding,
320 e -> {
321 setCaretAnimating(false);
322 eventHandler.handle(e);
323 setCaretAnimating(true);
324 },
325 interceptor);
326 }
327
328
329
330
331
332 /**************************************************************************
333 * Disposal methods *
334 *************************************************************************/
335
336 @Override public void dispose() {
337 textInputControl.textProperty().removeListener(textListener);
338 super.dispose();
339 }
340
341 /**************************************************************************
342 * Abstract methods *
343 *************************************************************************/
344
345 protected abstract void deleteChar(boolean previous);
346 protected abstract void replaceText(int start, int end, String txt);
347 protected abstract void setCaretAnimating(boolean play);
348 protected abstract void deleteFromLineStart();
349
350 protected abstract void mousePressed(MouseEvent e);
351 protected abstract void mouseDragged(MouseEvent e);
352 protected abstract void mouseReleased(MouseEvent e);
353 protected abstract void contextMenuRequested(ContextMenuEvent e);
354
355 /**************************************************************************
356 * Key handling implementation *
357 *************************************************************************/
358
359 /**
360 * The default handler for a key typed event, which is called when none of
361 * the other key bindings match. This is the method which handles basic
362 * text entry.
363 * @param event not null
364 */
365 private void defaultKeyTyped(KeyEvent event) {
366 final TextInputControl textInput = getNode();
367 // I'm not sure this case can actually ever happen, maybe this
368 // should be an assert instead?
369 if (!textInput.isEditable() || textInput.isDisabled()) return;
370
371 // Sometimes we get events with no key character, in which case
372 // we need to bail.
373 String character = event.getCharacter();
374 if (character.length() == 0) return;
375
376 // Filter out control keys except control+Alt on PC or Alt on Mac
377 if (event.isControlDown() || event.isAltDown() || (isMac() && event.isMetaDown())) {
378 if (!((event.isControlDown() || isMac()) && event.isAltDown())) return;
379 }
380
381 setEditing(true);
382
383 // Ignore characters in the control range and the ASCII delete
384 // character as well as meta key presses
385 if (character.charAt(0) > 0x1F
386 && character.charAt(0) != 0x7F
387 && !event.isMetaDown()) { // Not sure about this one
388 final IndexRange selection = textInput.getSelection();
389 final int start = selection.getStart();
390 final int end = selection.getEnd();
391
392 replaceText(start, end, character);
393 }
394
395 setEditing(false);
396 }
397
398 private Bidi bidi = null;
399 private Boolean mixed = null;
400 private Boolean rtlText = null;
401
402 private void invalidateBidi() {
403 bidi = null;
404 mixed = null;
405 rtlText = null;
406 }
407
408 private Bidi getBidi() {
409 if (bidi == null) {
410 bidi = new Bidi(textInputControl.textProperty().getValueSafe(),
411 (textInputControl.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT)
412 ? Bidi.DIRECTION_RIGHT_TO_LEFT
413 : Bidi.DIRECTION_LEFT_TO_RIGHT);
414 }
415 return bidi;
418 protected boolean isMixed() {
419 if (mixed == null) {
420 mixed = getBidi().isMixed();
421 }
422 return mixed;
423 }
424
425 protected boolean isRTLText() {
426 if (rtlText == null) {
427 Bidi bidi = getBidi();
428 rtlText =
429 (bidi.isRightToLeft() ||
430 (isMixed() &&
431 textInputControl.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT));
432 }
433 return rtlText;
434 }
435
436 private void nextCharacterVisually(boolean moveRight) {
437 if (isMixed()) {
438 TextInputControlSkin<?> skin = (TextInputControlSkin<?>)textInputControl.getSkin();
439 skin.moveCaret(TextUnit.CHARACTER, moveRight ? Direction.RIGHT : Direction.LEFT, false);
440 } else if (moveRight != isRTLText()) {
441 textInputControl.forward();
442 } else {
443 textInputControl.backward();
444 }
445 }
446
447 private void selectLeft() {
448 if (isRTLText()) {
449 textInputControl.selectForward();
450 } else {
451 textInputControl.selectBackward();
452 }
453 }
454
455 private void selectRight() {
456 if (isRTLText()) {
457 textInputControl.selectBackward();
458 } else {
459 textInputControl.selectForward();
460 }
461 }
462
463 private void deletePreviousChar() {
464 setEditing(true);
465 deleteChar(true);
466 setEditing(false);
467 }
468
469 private void deleteNextChar() {
470 setEditing(true);
471 deleteChar(false);
472 setEditing(false);
473 }
474
475 protected void deletePreviousWord() {
476 setEditing(true);
477 TextInputControl textInputControl = getNode();
478 int end = textInputControl.getCaretPosition();
479
480 if (end > 0) {
481 textInputControl.previousWord();
482 int start = textInputControl.getCaretPosition();
483 replaceText(start, end, "");
484 }
485 setEditing(false);
486 }
487
488 protected void deleteNextWord() {
489 setEditing(true);
490 TextInputControl textInputControl = getNode();
491 int start = textInputControl.getCaretPosition();
492
493 if (start < textInputControl.getLength()) {
494 nextWord();
495 int end = textInputControl.getCaretPosition();
496 replaceText(start, end, "");
497 }
498 setEditing(false);
499 }
500
501 public void deleteSelection() {
502 setEditing(true);
503 TextInputControl textInputControl = getNode();
504 IndexRange selection = textInputControl.getSelection();
505
506 if (selection.getLength() > 0) {
507 deleteChar(false);
508 }
509 setEditing(false);
510 }
511
512 public void cut() {
513 setEditing(true);
514 getNode().cut();
515 setEditing(false);
516 }
517
518 public void paste() {
519 setEditing(true);
520 getNode().paste();
521 setEditing(false);
522 }
523
524 public void undo() {
525 setEditing(true);
526 getNode().undo();
527 setEditing(false);
528 }
529
530 public void redo() {
531 setEditing(true);
532 getNode().redo();
533 setEditing(false);
534 }
535
536 protected void selectPreviousWord() {
537 getNode().selectPreviousWord();
538 }
539
540 public void selectNextWord() {
541 TextInputControl textInputControl = getNode();
542 if (isMac() || isLinux()) {
543 textInputControl.selectEndOfNextWord();
544 } else {
545 textInputControl.selectNextWord();
546 }
547 }
548
549 private void selectLeftWord() {
550 if (isRTLText()) {
551 selectNextWord();
552 } else {
553 selectPreviousWord();
554 }
555 }
556
557 private void selectRightWord() {
558 if (isRTLText()) {
559 selectPreviousWord();
560 } else {
561 selectNextWord();
562 }
563 }
564
565 protected void selectWord() {
566 final TextInputControl textInputControl = getNode();
567 textInputControl.previousWord();
568 if (isWindows()) {
569 textInputControl.selectNextWord();
570 } else {
571 textInputControl.selectEndOfNextWord();
572 }
573 }
574
575 protected void previousWord() {
576 getNode().previousWord();
577 }
578
579 protected void nextWord() {
580 TextInputControl textInputControl = getNode();
581 if (isMac() || isLinux()) {
582 textInputControl.endOfNextWord();
583 } else {
584 textInputControl.nextWord();
585 }
586 }
587
588 private void leftWord() {
589 if (isRTLText()) {
590 nextWord();
591 } else {
592 previousWord();
593 }
594 }
595
596 private void rightWord() {
597 if (isRTLText()) {
598 previousWord();
599 } else {
600 nextWord();
601 }
602 }
603
604 protected void fire(KeyEvent event) { } // TODO move to TextFieldBehavior
605 protected void cancelEdit(KeyEvent event) { forwardToParent(event);}
606
607 protected void forwardToParent(KeyEvent event) {
608 if (getNode().getParent() != null) {
609 getNode().getParent().fireEvent(event);
610 }
611 }
612
613 protected void selectHome() {
614 getNode().selectHome();
615 }
616
617 protected void selectEnd() {
618 getNode().selectEnd();
619 }
620
621 protected void selectHomeExtend() {
622 getNode().extendSelection(0);
623 }
624
625 protected void selectEndExtend() {
626 TextInputControl textInputControl = getNode();
627 textInputControl.extendSelection(textInputControl.getLength());
628 }
629
630 private boolean editing = false;
631 protected void setEditing(boolean b) {
632 editing = b;
633 }
634 public boolean isEditing() {
635 return editing;
636 }
637
638 protected void populateContextMenu(ContextMenu contextMenu) {
639 TextInputControl textInputControl = getNode();
640 boolean editable = textInputControl.isEditable();
641 boolean hasText = (textInputControl.getLength() > 0);
642 boolean hasSelection = (textInputControl.getSelection().getLength() > 0);
643 boolean maskText = (textInputControl instanceof PasswordField); // (maskText("A") != "A");
644 ObservableList<MenuItem> items = contextMenu.getItems();
645
646 if (SHOW_HANDLES) {
647 items.clear();
648 if (!maskText && hasSelection) {
649 if (editable) {
650 items.add(cutMI);
651 }
652 items.add(copyMI);
653 }
654 if (editable && Clipboard.getSystemClipboard().hasString()) {
655 items.add(pasteMI);
656 }
657 if (hasText) {
658 if (!hasSelection) {
659 items.add(selectWordMI);
660 }
661 items.add(selectAllMI);
662 }
663 selectWordMI.getProperties().put("refreshMenu", Boolean.TRUE);
664 selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
665 } else {
666 if (editable) {
667 items.setAll(undoMI, redoMI, cutMI, copyMI, pasteMI, deleteMI,
668 separatorMI, selectAllMI);
669 } else {
670 items.setAll(copyMI, separatorMI, selectAllMI);
671 }
672 undoMI.setDisable(!getNode().isUndoable());
673 redoMI.setDisable(!getNode().isRedoable());
674 cutMI.setDisable(maskText || !hasSelection);
675 copyMI.setDisable(maskText || !hasSelection);
676 pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString());
677 deleteMI.setDisable(!hasSelection);
678 }
679 }
680
681 private static class ContextMenuItem extends MenuItem {
682 ContextMenuItem(final String action, EventHandler<ActionEvent> onAction) {
683 super(getString("TextInputControl.menu." + action));
684 setOnAction(onAction);
685 }
686 }
687
688 private final MenuItem undoMI = new ContextMenuItem("Undo", e -> undo());
689 private final MenuItem redoMI = new ContextMenuItem("Redo", e -> redo());
690 private final MenuItem cutMI = new ContextMenuItem("Cut", e -> cut());
691 private final MenuItem copyMI = new ContextMenuItem("Copy", e -> getNode().copy());
692 private final MenuItem pasteMI = new ContextMenuItem("Paste", e -> paste());
693 private final MenuItem deleteMI = new ContextMenuItem("DeleteSelection", e -> deleteSelection());
694 private final MenuItem selectWordMI = new ContextMenuItem("SelectWord", e -> selectNextWord());
695 private final MenuItem selectAllMI = new ContextMenuItem("SelectAll", e -> getNode().selectAll());
696 private final MenuItem separatorMI = new SeparatorMenuItem();
697
698 }
|