Skip to content

Commit 7552c74

Browse files
authored
Allow ALT and ALT + CONTROL (or ALTGR on Windows) accelerators. (#922)
1 parent fe947d0 commit 7552c74

2 files changed

Lines changed: 98 additions & 18 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.fxmisc.richtext;
2+
3+
import javafx.event.EventTarget;
4+
import javafx.scene.input.KeyCode;
5+
import javafx.scene.input.KeyEvent;
6+
import org.junit.Test;
7+
8+
import static org.junit.Assert.assertEquals;
9+
10+
//This class requires to be in this package, as it requires access to GenericStyledAreaBehavior.
11+
public class AcceleratorsTests extends InlineCssTextAreaAppTest {
12+
13+
@Test
14+
public void typing_alt_control_combinations_dont_consume_events_if_they_dont_have_any_character_assigned() {
15+
AcceleratorsTestsHelper[] events;
16+
17+
if (System.getProperty("os.name").startsWith("Windows")) {
18+
//WINDOWS TESTS
19+
events = new AcceleratorsTestsHelper[]{
20+
//CHARACTER WITHOUT MODIFIERS
21+
new AcceleratorsTestsHelper(area, "f", KeyCode.F, false, false, true),
22+
//CHARACTER WITH CONTROL
23+
new AcceleratorsTestsHelper(area, "\0", KeyCode.F, true, false, false),
24+
//CHARACTER WITH ALT
25+
new AcceleratorsTestsHelper(area, "\0", KeyCode.F, false, true, false),
26+
//CHARACTER WITH ALT + CONTROL / ALTGR on Windows
27+
new AcceleratorsTestsHelper(area, "\0", KeyCode.K, true, true, false),
28+
//ALT + CONTROL / ALTGR on Windows with an assigned special character (E -> Euro on spanish keyboard)
29+
new AcceleratorsTestsHelper(area, "\u20AC", KeyCode.E, true, true, true)
30+
};
31+
} else {
32+
//LINUX TESTS
33+
events = new AcceleratorsTestsHelper[]{
34+
//CHARACTER WITHOUT MODIFIERS
35+
new AcceleratorsTestsHelper(area, "f", KeyCode.F, false, false, true),
36+
//CHARACTER WITH CONTROL
37+
new AcceleratorsTestsHelper(area, "\0", KeyCode.F, true, false, false),
38+
//CHARACTER WITH ALT
39+
new AcceleratorsTestsHelper(area, "\0", KeyCode.F, false, true, false),
40+
//CHARACTER WITH ALT + CONTROL
41+
new AcceleratorsTestsHelper(area, "\0", KeyCode.F, true, true, false),
42+
};
43+
}
44+
AcceleratorsTestsHelper helper;
45+
for (int i = 0; i < events.length; i++) {
46+
helper = events[i];
47+
assertEquals("Event " + i + " unexpected result.", helper.expectedConsumeResult,
48+
GenericStyledAreaBehavior.isControlKeyEvent(helper.keyEvent));
49+
}
50+
}
51+
52+
/**
53+
* Small helper class. Allows to make tests faster.
54+
*/
55+
private static class AcceleratorsTestsHelper {
56+
57+
KeyEvent keyEvent;
58+
boolean expectedConsumeResult;
59+
60+
public AcceleratorsTestsHelper(EventTarget source, String character, KeyCode key, boolean controlDown, boolean altDown, boolean expected) {
61+
keyEvent = new KeyEvent(source, source, KeyEvent.KEY_TYPED, character, key.getName(), key,
62+
false, controlDown, altDown, false);
63+
expectedConsumeResult = expected;
64+
}
65+
}
66+
}

richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledAreaBehavior.java

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.fxmisc.wellbehaved.event.template.InputMapTemplate.*;
99
import static org.reactfx.EventStreams.*;
1010

11+
import java.util.Arrays;
1112
import java.util.function.Predicate;
1213

1314
import javafx.event.Event;
@@ -44,6 +45,7 @@ class GenericStyledAreaBehavior {
4445
}
4546

4647
private static final InputMapTemplate<GenericStyledAreaBehavior, ? super Event> EVENT_TEMPLATE;
48+
private static final Predicate<KeyEvent> controlKeysFilter;
4749

4850
static {
4951
SelectionPolicy selPolicy = isMac
@@ -65,7 +67,7 @@ class GenericStyledAreaBehavior {
6567
KeyCharacterCombination SHORTCUT_Y = new KeyCharacterCombination( "y", SHORTCUT_DOWN );
6668
KeyCharacterCombination SHORTCUT_Z = new KeyCharacterCombination( "z", SHORTCUT_DOWN );
6769
KeyCharacterCombination SHORTCUT_SHIFT_Z = new KeyCharacterCombination( "z", SHORTCUT_DOWN, SHIFT_DOWN );
68-
70+
6971
InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> editsBase = sequence(
7072
// deletion
7173
consume(keyPressed(DELETE), GenericStyledAreaBehavior::deleteForward),
@@ -167,18 +169,26 @@ class GenericStyledAreaBehavior {
167169
), (b, e) -> b.view.copy()
168170
);
169171

170-
Predicate<KeyEvent> noControlKeys = e ->
171-
// filter out control keys
172-
(!e.isControlDown() && !e.isMetaDown())
173-
// except on Windows allow the Ctrl+Alt combination (produced by AltGr)
174-
|| (isWindows && !e.isMetaDown() && (!e.isControlDown() || e.isAltDown()));
172+
controlKeysFilter = e -> {
173+
if (isWindows) {
174+
//Windows input. ALT + CONTROL accelerators are the same as ALT GR accelerators.
175+
//If ALT + CONTROL are pressed and the given character is valid then print the character.
176+
//Else, don't consume the event. This change allows Windows users to use accelerators and
177+
//printing special characters at the same time.
178+
// (For example: ALT + CONTROL + E prints the euro symbol in the spanish keyboard while ALT + CONTROL + L has assigned an accelerator.)
179+
//Note that this is how several IDEs such JetBrains IDEs or Eclipse behave.
180+
if (e.isControlDown() && e.isAltDown() && !e.isMetaDown() && e.getCharacter().length() == 1
181+
&& e.getCharacter().getBytes()[0] != 0) return true;
182+
}
183+
return !e.isControlDown() && !e.isAltDown() && !e.isMetaDown();
184+
};
175185

176186
Predicate<KeyEvent> isChar = e ->
177187
e.getCode().isLetterKey() ||
178188
e.getCode().isDigitKey() ||
179189
e.getCode().isWhitespaceKey();
180190

181-
InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> charPressConsumer = consume(keyPressed().onlyIf(isChar.and(noControlKeys)));
191+
InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> charPressConsumer = consume(keyPressed().onlyIf(isChar.and(controlKeysFilter)));
182192

183193
InputMapTemplate<GenericStyledAreaBehavior, ? super KeyEvent> keyPressedTemplate = edits
184194
.orElse(otherNavigation).ifConsumed((b, e) -> b.view.clearTargetCaretOffset())
@@ -191,7 +201,7 @@ class GenericStyledAreaBehavior {
191201

192202
InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> keyTypedBase = consume(
193203
// character input
194-
EventPattern.keyTyped().onlyIf(noControlKeys.and(e -> isLegal(e.getCharacter()))),
204+
EventPattern.keyTyped().onlyIf(controlKeysFilter.and(e -> isLegal(e.getCharacter()))),
195205
GenericStyledAreaBehavior::keyTyped
196206
).ifConsumed((b, e) -> b.view.requestFollowCaret());
197207
InputMapTemplate<GenericStyledAreaBehavior, ? super KeyEvent> keyTypedTemplate = when(b -> b.view.isEditable(), keyTypedBase);
@@ -344,16 +354,6 @@ private void keyTyped(KeyEvent event) {
344354
view.replaceSelection(text);
345355
}
346356

347-
private static boolean isLegal(String text) {
348-
int n = text.length();
349-
for(int i = 0; i < n; ++i) {
350-
if(Character.isISOControl(text.charAt(i))) {
351-
return false;
352-
}
353-
}
354-
return true;
355-
}
356-
357357
private void deleteBackward(KeyEvent ignore) {
358358
IndexRange selection = view.getSelection();
359359
if(selection.getLength() == 0) {
@@ -585,4 +585,18 @@ private static Point2D project(Point2D p, Bounds bounds) {
585585
private static double clamp(double x, double min, double max) {
586586
return Math.min(Math.max(x, min), max);
587587
}
588+
589+
static boolean isControlKeyEvent(KeyEvent event) {
590+
return controlKeysFilter.test(event);
591+
}
592+
593+
private static boolean isLegal(String text) {
594+
int n = text.length();
595+
for(int i = 0; i < n; ++i) {
596+
if(Character.isISOControl(text.charAt(i))) {
597+
return false;
598+
}
599+
}
600+
return true;
601+
}
588602
}

0 commit comments

Comments
 (0)