diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java index f7848165e8e..e91e455fe04 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java @@ -751,6 +751,32 @@ public static int getKeyCodeForChar(char c) { return application._getKeyCodeForChar(c); } + /** + * The default implementation bridges to the existing getKeyCodeForChar call. Platform + * instances are expected to override this call. + */ + protected boolean _getKeyCanGenerateCharacter(int hardwareCode, int vkCode, char c) { + if (vkCode != com.sun.glass.events.KeyEvent.VK_UNDEFINED) { + return getKeyCodeForChar(c) == vkCode; + } + return false; + } + + /** + * Returns true if the key is capable of producing the given Unicode + * character. The call will be provided enough information to identify the + * key, either a vkCode that is not VK_UNDEFINED or a hardwareCode that is + * non-negative or both. + * + * @param hardwareCode the platform-specific key identifier + * @param vkCode the JavaFX key code + * @param c the character + * @return true if the key can generate the character + */ + public final boolean getKeyCanGenerateCharacter(int hardwareCode, int vkCode, char c) { + return _getKeyCanGenerateCharacter(hardwareCode, vkCode, c); + } + protected int _isKeyLocked(int keyCode) { // Overridden in subclasses return KeyEvent.KEY_LOCK_UNKNOWN; diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java index 5ee23b2b4a6..3c189be972c 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java @@ -65,8 +65,11 @@ public abstract class View { public static class EventHandler { public void handleViewEvent(View view, long time, int type) { } - public void handleKeyEvent(View view, long time, int action, - int keyCode, char[] keyChars, int modifiers) { + public boolean handleKeyEvent(View view, long time, int action, + int keyCode, char[] keyChars, int modifiers, int hardwareCode) + { + /* Event was not consumed */ + return false; } public void handleMenuEvent(View view, int x, int y, int xAbs, int yAbs, boolean isKeyboardTrigger) { @@ -536,11 +539,13 @@ private void handleViewEvent(long time, int type) { } } - private void handleKeyEvent(long time, int action, - int keyCode, char[] keyChars, int modifiers) { + private boolean handleKeyEvent(long time, int action, + int keyCode, char[] keyChars, int modifiers, int hardwareCode) { if (this.eventHandler != null) { - this.eventHandler.handleKeyEvent(this, time, action, keyCode, keyChars, modifiers); + return this.eventHandler.handleKeyEvent(this, time, action, keyCode, + keyChars, modifiers, hardwareCode); } + return false; } private void handleMouseEvent(long time, int type, int button, int x, int y, @@ -963,7 +968,12 @@ protected void notifyScroll(int x, int y, int xAbs, int yAbs, } protected void notifyKey(int type, int keyCode, char[] keyChars, int modifiers) { - handleKeyEvent(System.nanoTime(), type, keyCode, keyChars, modifiers); + handleKeyEvent(System.nanoTime(), type, keyCode, keyChars, modifiers, -1); + } + + // Returns true iff event was consumed + protected boolean notifyKeyEx(int type, int keyCode, char[] keyChars, int modifiers, int hardwareCode) { + return handleKeyEvent(System.nanoTime(), type, keyCode, keyChars, modifiers, hardwareCode); } protected void notifyInputMethod(String text, int[] clauseBoundary, diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java index 45ca63fa4e1..27cb73b96f5 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java @@ -462,7 +462,14 @@ protected boolean _supportsInputMethods() { } @Override - protected native int _getKeyCodeForChar(char c); + protected int _getKeyCodeForChar(char c) { + // Linux has transitioned to getKeyCanGenerateCharacter so this + // should never be called. + return 0; + } + + @Override + protected native boolean _getKeyCanGenerateCharacter(int hardwareCode, int vkCode, char c); @Override protected native int _isKeyLocked(int keyCode); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/SceneHelper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/SceneHelper.java index 3303d1826f8..e2a44a1dce1 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/SceneHelper.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/SceneHelper.java @@ -53,8 +53,8 @@ public static void enableInputMethodEvents(Scene scene, boolean enable) { sceneAccessor.enableInputMethodEvents(scene, enable); } - public static void processKeyEvent(Scene scene, KeyEvent e) { - sceneAccessor.processKeyEvent(scene, e); + public static boolean processKeyEvent(Scene scene, KeyEvent e) { + return sceneAccessor.processKeyEvent(scene, e); } public static void processMouseEvent(Scene scene, MouseEvent e) { @@ -118,7 +118,7 @@ public static SceneAccessor getSceneAccessor() { public interface SceneAccessor { void enableInputMethodEvents(Scene scene, boolean enable); - void processKeyEvent(Scene scene, KeyEvent e); + boolean processKeyEvent(Scene scene, KeyEvent e); void processMouseEvent(Scene scene, MouseEvent e); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/input/KeyEventHelper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/input/KeyEventHelper.java new file mode 100644 index 00000000000..6d407c7aa47 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/input/KeyEventHelper.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.scene.input; + +import com.sun.javafx.util.Utils; +import javafx.scene.input.KeyEvent; + +/** + * Used to access internal methods of KeyEvent. + */ +public class KeyEventHelper { + + private static KeyEventAccessor keyEventAccessor; + + static { + Utils.forceInit(KeyEvent.class); + } + + private KeyEventHelper() { + } + + public static void setHardwareCode(KeyEvent keyEvent, int hardwareCode) { + keyEventAccessor.setHardwareCode(keyEvent, hardwareCode); + } + + public static int getHardwareCode(KeyEvent keyEvent) { + return keyEventAccessor.getHardwareCode(keyEvent); + } + + public static void setKeyEventAccessor(final KeyEventAccessor newAccessor) { + if (keyEventAccessor != null) { + throw new IllegalStateException(); + } + + keyEventAccessor = newAccessor; + } + + public interface KeyEventAccessor { + void setHardwareCode(KeyEvent keyEvent, int hardwareCode); + int getHardwareCode(KeyEvent keyEvent); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java index 348cc8527a8..5c0aaf27869 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java @@ -63,8 +63,11 @@ public void mouseEvent(EventType type, double x, double y, double sc /** * Pass a key event to the scene to handle + * + * @param keyEvent The key event + * @return true iff the event was consumed */ - public void keyEvent(KeyEvent keyEvent); + public boolean keyEvent(KeyEvent keyEvent); /** * Pass an input method event to the scene to handle diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/Toolkit.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/Toolkit.java index e06b8da829f..816b89764a5 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/Toolkit.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/Toolkit.java @@ -703,6 +703,15 @@ protected static final double clampStopOffset(double offset) { float dashOffset); public abstract int getKeyCodeForChar(String character); + /** + * The default implementation bridges into the existing getKeyCodeForChar call. + */ + public boolean getKeyCanGenerateCharacter(KeyEvent event, String character) { + if (event.getCode() != KeyCode.UNDEFINED) { + return getKeyCodeForChar(character) == event.getCode().getCode(); + } + return false; + } public abstract Dimension2D getBestCursorSize(int preferredWidth, int preferredHeight); public abstract int getMaximumCursorColors(); public abstract PathElement[] convertShapeToFXPath(Object shape); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java index a1b43c7073b..a5aaeb3976a 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java @@ -41,6 +41,7 @@ import com.sun.javafx.collections.TrackableObservableList; import com.sun.javafx.logging.PulseLogger; import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; +import com.sun.javafx.scene.input.KeyEventHelper; import com.sun.javafx.scene.input.KeyCodeMap; import javafx.collections.ListChangeListener; @@ -147,22 +148,25 @@ private static EventType keyEventType(int glassType } private final KeyEventNotification keyNotification = new KeyEventNotification(); - private class KeyEventNotification implements PrivilegedAction { + private class KeyEventNotification implements PrivilegedAction { View view; long time; int type; int key; char[] chars; int modifiers; + int hardwareCode; private KeyCode lastKeyCode; @Override - public Void run() { + public Boolean run() { if (PULSE_LOGGING_ENABLED) { PulseLogger.newInput(keyEventType(type).toString()); } WindowStage stage = scene.getWindowStage(); + Boolean consumed = Boolean.FALSE; + try { boolean shiftDown = (modifiers & KeyEvent.MODIFIER_SHIFT) != 0; boolean controlDown = (modifiers & KeyEvent.MODIFIER_CONTROL) != 0; @@ -177,6 +181,7 @@ public Void run() { str, text, KeyCodeMap.valueOf(key) , shiftDown, controlDown, altDown, metaDown); + KeyEventHelper.setHardwareCode(keyEvent, hardwareCode); KeyCode keyCode = KeyCodeMap.valueOf(key); switch (type) { @@ -215,7 +220,8 @@ public Void run() { } } if (scene.sceneListener != null) { - scene.sceneListener.keyEvent(keyEvent); + if (scene.sceneListener.keyEvent(keyEvent)) + consumed = Boolean.TRUE; } break; default: @@ -231,13 +237,13 @@ public Void run() { PulseLogger.newInput(null); } } - return null; + return consumed; } } @SuppressWarnings("removal") - @Override public void handleKeyEvent(View view, long time, int type, int key, - char[] chars, int modifiers) + @Override public boolean handleKeyEvent(View view, long time, int type, int key, + char[] chars, int modifiers, int hardwareCode) { keyNotification.view = view; keyNotification.time = time; @@ -245,8 +251,9 @@ public Void run() { keyNotification.key = key; keyNotification.chars = chars; keyNotification.modifiers = modifiers; + keyNotification.hardwareCode = hardwareCode; - QuantumToolkit.runWithoutRenderLock(() -> { + return QuantumToolkit.runWithoutRenderLock(() -> { return AccessController.doPrivileged(keyNotification, scene.getAccessControlContext()); }); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java index 14205cf5a7e..33ceb33d294 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java @@ -95,6 +95,7 @@ import com.sun.javafx.perf.PerformanceTracker; import com.sun.javafx.runtime.async.AbstractRemoteResource; import com.sun.javafx.runtime.async.AsyncOperationListener; +import com.sun.javafx.scene.input.KeyEventHelper; import com.sun.javafx.scene.text.TextLayoutFactory; import com.sun.javafx.sg.prism.NGNode; import com.sun.javafx.tk.CompletionListener; @@ -1086,6 +1087,20 @@ public Shape createStrokedShape(Shape shape, : com.sun.glass.events.KeyEvent.VK_UNDEFINED; } + // The Quantum version of this call knows that we may have the hardware key code + // available. + @Override public boolean getKeyCanGenerateCharacter(KeyEvent keyEvent, String character) { + int hardwareCode = KeyEventHelper.getHardwareCode(keyEvent); + if (keyEvent.getCode() != KeyCode.UNDEFINED || hardwareCode != -1) { + if (character.length() == 1) + return Application.GetApplication().getKeyCanGenerateCharacter( + hardwareCode, + keyEvent.getCode().getCode(), + character.charAt(0)); + } + return false; + } + @Override public PathElement[] convertShapeToFXPath(Object shape) { if (shape == null) { return new PathElement[0]; diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java index 55c87cb28df..4123b99dd69 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java @@ -35,6 +35,7 @@ import com.sun.javafx.css.StyleManager; import com.sun.javafx.cursor.CursorFrame; import com.sun.javafx.event.EventQueue; +import com.sun.javafx.event.EventUtil; import com.sun.javafx.geom.PickRay; import com.sun.javafx.geom.Vec3d; import com.sun.javafx.geom.transform.BaseTransform; @@ -397,8 +398,8 @@ public void enableInputMethodEvents(Scene scene, boolean enable) { } @Override - public void processKeyEvent(Scene scene, KeyEvent e) { - scene.processKeyEvent(e); + public boolean processKeyEvent(Scene scene, KeyEvent e) { + return scene.processKeyEvent(e); } @Override @@ -2178,7 +2179,10 @@ private void focusIneligible(Node node) { traverse(node, Direction.NEXT, TraversalMethod.DEFAULT); } - void processKeyEvent(KeyEvent e) { + /** + * @return true iff the event was consumed + */ + boolean processKeyEvent(KeyEvent e) { if (dndGesture != null) { if (!dndGesture.processKey(e)) { dndGesture = null; @@ -2191,7 +2195,7 @@ void processKeyEvent(KeyEvent e) { // send the key event to the current focus owner or to scene if // the focus owner is not set - Event.fireEvent(eventTarget, e); + return EventUtil.fireEvent(eventTarget, e) == null; } void requestFocus(Node node, boolean focusVisible) { @@ -2710,9 +2714,9 @@ public void mouseEvent(EventType type, double x, double y, double sc @Override - public void keyEvent(KeyEvent keyEvent) + public boolean keyEvent(KeyEvent keyEvent) { - processKeyEvent(keyEvent); + return processKeyEvent(keyEvent); } @Override diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/input/KeyCharacterCombination.java b/modules/javafx.graphics/src/main/java/javafx/scene/input/KeyCharacterCombination.java index bd2dfe468ac..6794fd5ffbc 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/input/KeyCharacterCombination.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/input/KeyCharacterCombination.java @@ -109,12 +109,8 @@ public KeyCharacterCombination(final @NamedArg("character") String character, */ @Override public boolean match(final KeyEvent event) { - if (event.getCode() == KeyCode.UNDEFINED) { - return false; - } - return (event.getCode().getCode() - == Toolkit.getToolkit().getKeyCodeForChar(getCharacter())) - && super.match(event); + return (super.match(event) && + Toolkit.getToolkit().getKeyCanGenerateCharacter(event, getCharacter())); } /** diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/input/KeyEvent.java b/modules/javafx.graphics/src/main/java/javafx/scene/input/KeyEvent.java index b97271d28f9..0326246f9f7 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/input/KeyEvent.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/input/KeyEvent.java @@ -26,6 +26,8 @@ package javafx.scene.input; import com.sun.javafx.tk.Toolkit; +import com.sun.javafx.scene.input.KeyEventHelper; + import javafx.beans.NamedArg; import javafx.event.EventTarget; import javafx.event.EventType; @@ -134,6 +136,7 @@ public KeyEvent(@NamedArg("source") Object source, @NamedArg("target") EventTarg this.controlDown = controlDown; this.altDown = altDown; this.metaDown = metaDown; + this.hardwareCode = -1; } /** @@ -162,6 +165,7 @@ public KeyEvent(@NamedArg("eventType") EventType eventType, @NamedArg( this.controlDown = controlDown; this.altDown = altDown; this.metaDown = metaDown; + this.hardwareCode = -1; } /** @@ -378,6 +382,32 @@ public EventType getEventType() { return (EventType) super.getEventType(); } + /** + * The hardware key code which is private to the implementation. + */ + private int hardwareCode; + int getHardwareCode() { + return hardwareCode; + } + void setHardwareCode(int newCode) { + hardwareCode = newCode; + } + + static { + KeyEventHelper.setKeyEventAccessor( + new KeyEventHelper.KeyEventAccessor() { + @Override + public void setHardwareCode(KeyEvent keyEvent, int hardwareCode) { + keyEvent.setHardwareCode(hardwareCode); + } + + @Override + public int getHardwareCode(KeyEvent keyEvent) { + return keyEvent.getHardwareCode(); + } + } + ); + } } diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp index 2d2e0fd553a..87c206bd68e 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp @@ -54,7 +54,7 @@ jmethodID jScreenNotifySettingsChanged; jmethodID jViewNotifyResize; jmethodID jViewNotifyMouse; jmethodID jViewNotifyRepaint; -jmethodID jViewNotifyKey; +jmethodID jViewNotifyKeyEx; jmethodID jViewNotifyView; jmethodID jViewNotifyDragEnter; jmethodID jViewNotifyDragOver; @@ -196,7 +196,7 @@ JNI_OnLoad(JavaVM *jvm, void *reserved) if (env->ExceptionCheck()) return JNI_ERR; jViewNotifyRepaint = env->GetMethodID(clazz, "notifyRepaint", "(IIII)V"); if (env->ExceptionCheck()) return JNI_ERR; - jViewNotifyKey = env->GetMethodID(clazz, "notifyKey", "(II[CI)V"); + jViewNotifyKeyEx = env->GetMethodID(clazz, "notifyKeyEx", "(II[CII)Z"); if (env->ExceptionCheck()) return JNI_ERR; jViewNotifyView = env->GetMethodID(clazz, "notifyView", "(I)V"); if (env->ExceptionCheck()) return JNI_ERR; diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h index c96500322ff..879141751eb 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h @@ -160,7 +160,7 @@ struct jni_exception: public std::exception { extern jmethodID jViewNotifyResize; // com.sun.glass.ui.View#notifyResize (II)V extern jmethodID jViewNotifyMouse; // com.sun.glass.ui.View#notifyMouse (IIIIIIIZZ)V extern jmethodID jViewNotifyRepaint; // com.sun.glass.ui.View#notifyRepaint (IIII)V - extern jmethodID jViewNotifyKey; // com.sun.glass.ui.View#notifyKey (II[CI)V + extern jmethodID jViewNotifyKeyEx; // com.sun.glass.ui.View#notifyKeyEx (II[CII)Z extern jmethodID jViewNotifyView; //com.sun.glass.ui.View#notifyView (I)V extern jmethodID jViewNotifyDragEnter; //com.sun.glass.ui.View#notifyDragEnter (IIIII)I extern jmethodID jViewNotifyDragOver; //com.sun.glass.ui.View#notifyDragOver (IIIII)I diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_key.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_key.cpp index f90970b66a7..2ea1e360a70 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_key.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_key.cpp @@ -319,16 +319,11 @@ jint glass_key_to_modifier(jint glassKey) { extern "C" { /* - * Class: com_sun_glass_ui_gtk_GtkApplication - * Method: _getKeyCodeForChar - * Signature: (C)I + * The original getKeyCodeForChar implementation. Superseded + * by getCanKeyGenerateCharacter. */ -JNIEXPORT jint JNICALL Java_com_sun_glass_ui_gtk_GtkApplication__1getKeyCodeForChar - (JNIEnv *env, jobject jApplication, jchar character) +static jint getKeyCodeForChar(jchar character) { - (void)env; - (void)jApplication; - gunichar *ucs_char = g_utf16_to_ucs4(&character, 1, NULL, NULL, NULL); if (ucs_char == NULL) { return com_sun_glass_events_KeyEvent_VK_UNDEFINED; @@ -365,6 +360,22 @@ static Bool isXkbAvailable(Display *display) { return xkbAvailable; } +/* + * Determine which keyboard layout is active. This is the group + * number in the Xkb state. There is no direct way to query this + * in Gdk. + */ + static gint get_current_keyboard_group() + { + Display* display = gdk_x11_display_get_xdisplay(gdk_display_get_default()); + if (isXkbAvailable(display)) { + XkbStateRec xkbState; + XkbGetState(display, XkbUseCoreKbd, &xkbState); + return xkbState.group; + } + return -1; + } + /* * Class: com_sun_glass_ui_gtk_GtkApplication * Method: _isKeyLocked @@ -405,4 +416,90 @@ JNIEXPORT jint JNICALL Java_com_sun_glass_ui_gtk_GtkApplication__1isKeyLocked return com_sun_glass_events_KeyEvent_KEY_LOCK_UNKNOWN; } +static bool vkCodeIsNumericKeypad(jint vkCode) +{ + switch (vkCode) { + case com_sun_glass_events_KeyEvent_VK_NUMPAD0: + case com_sun_glass_events_KeyEvent_VK_NUMPAD1: + case com_sun_glass_events_KeyEvent_VK_NUMPAD2: + case com_sun_glass_events_KeyEvent_VK_NUMPAD3: + case com_sun_glass_events_KeyEvent_VK_NUMPAD4: + case com_sun_glass_events_KeyEvent_VK_NUMPAD5: + case com_sun_glass_events_KeyEvent_VK_NUMPAD6: + case com_sun_glass_events_KeyEvent_VK_NUMPAD7: + case com_sun_glass_events_KeyEvent_VK_NUMPAD8: + case com_sun_glass_events_KeyEvent_VK_NUMPAD9: + case com_sun_glass_events_KeyEvent_VK_DIVIDE: + case com_sun_glass_events_KeyEvent_VK_MULTIPLY: + case com_sun_glass_events_KeyEvent_VK_SUBTRACT: + case com_sun_glass_events_KeyEvent_VK_ADD: + case com_sun_glass_events_KeyEvent_VK_DECIMAL: + case com_sun_glass_events_KeyEvent_VK_SEPARATOR: + return true; + default: + return false; + } +} + +JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkApplication__1getKeyCanGenerateCharacter + (JNIEnv *env, jobject jApplication, jint hardwareCode, jint vkCode, jchar character) +{ + (void)env; + (void)jApplication; + + if (hardwareCode < 0) { + /* + * This key event didn't originate from this platform code (someone + * constructed a KeyEvent from whole cloth). Use the old code. + */ + if (vkCode != com_sun_glass_events_KeyEvent_VK_UNDEFINED) { + return vkCode == getKeyCodeForChar(character); + } + return FALSE; + } + + gint currentGroup = get_current_keyboard_group(); + if (currentGroup < 0) { + return FALSE; + } + + /* + * Walk through modifier states looking for the character. + */ + static const GdkModifierType standardModifiers[] = { + (GdkModifierType) 0, + GDK_SHIFT_MASK, + GDK_MOD5_MASK, // AltGr + (GdkModifierType) ((int) GDK_MOD5_MASK | (int) GDK_SHIFT_MASK) + }; + static const GdkModifierType keypadModifiers[] = { + GDK_MOD2_MASK + }; + + int numModifiers = 4; + const GdkModifierType* modifierArray = standardModifiers; + if (vkCodeIsNumericKeypad(vkCode)) { + numModifiers = 1; + modifierArray = keypadModifiers; + } + + GdkKeymap* keymap = gdk_keymap_get_for_display(gdk_display_get_default()); + for (int i = 0; i < numModifiers; ++i) { + guint keyValue = 0; + if (!gdk_keymap_translate_keyboard_state(keymap, hardwareCode, modifierArray[i], + currentGroup, &keyValue, NULL, NULL, NULL)) { + // Failure at this modifier level means we'll fail at higher levels. + return FALSE; + } + if (keyValue != 0) { + guint32 unicode = gdk_keyval_to_unicode(keyValue); + if (unicode == character) { + return TRUE; + } + } + } + + return FALSE; +} + } // extern "C" diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp index d1babd1a28a..6183c36c31c 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp @@ -488,27 +488,30 @@ void WindowContextBase::process_key(GdkEventKey* event) { } if (jview) { if (press) { - mainEnv->CallVoidMethod(jview, jViewNotifyKey, + mainEnv->CallBooleanMethod(jview, jViewNotifyKeyEx, com_sun_glass_events_KeyEvent_PRESS, glassKey, jChars, - glassModifier); + glassModifier, + event->hardware_keycode); CHECK_JNI_EXCEPTION(mainEnv) if (jview && key > 0) { // TYPED events should only be sent for printable characters. - mainEnv->CallVoidMethod(jview, jViewNotifyKey, + mainEnv->CallBooleanMethod(jview, jViewNotifyKeyEx, com_sun_glass_events_KeyEvent_TYPED, com_sun_glass_events_KeyEvent_VK_UNDEFINED, jChars, - glassModifier); + glassModifier, + event->hardware_keycode); CHECK_JNI_EXCEPTION(mainEnv) } } else { - mainEnv->CallVoidMethod(jview, jViewNotifyKey, + mainEnv->CallVoidMethod(jview, jViewNotifyKeyEx, com_sun_glass_events_KeyEvent_RELEASE, glassKey, jChars, - glassModifier); + glassModifier, + event->hardware_keycode); CHECK_JNI_EXCEPTION(mainEnv) } } diff --git a/tests/manual/events/.classpath b/tests/manual/events/.classpath index eb25bab3655..de056e00d10 100644 --- a/tests/manual/events/.classpath +++ b/tests/manual/events/.classpath @@ -5,6 +5,11 @@ + + + + + diff --git a/tests/manual/events/KeyCharacterCombinationTest.java b/tests/manual/events/KeyCharacterCombinationTest.java new file mode 100644 index 00000000000..e2437477db5 --- /dev/null +++ b/tests/manual/events/KeyCharacterCombinationTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.control.TextArea; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyCharacterCombination; +import javafx.scene.input.KeyEvent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +/* + * To run this test press keys on the main keyboard or numeric keypad that + * generate printable characters. The app will verify that + * KeyCharacterCombinations targeting those characters match the KEY_PRESSED + * events that generated them. For each key press you should see a "Passed" or + * "Failed" message detailing the outcome. For a key that doesn't generate a + * printable character (like Home or Tab) you should see an "Ignored" message. + * + * KeyCharacterCombinations are primarily used to define accelerators involving + * punctuation or symbols (all other accelerators should be defined using + * KeyCodeCombinations) so it's recommended that you focus your testing there. + * + * This test is most likely to fail on punctuation and symbols typed using the + * Shift key. For example, on a U.S. English keyboard pressing Shift+"=" to + * generate a "+" symbol may fail. Keys are also more likely to fail on layouts + * other than U.S. English. + * + * Dead keys will confuse this application. There is variation in how they're + * handled across platforms and there's no easy way to detect when one is + * pressed. Try to avoid them. + * + * On a platform with a working Robot implementation the KeyboardTest.java app + * in tests/manual/events covers more keys more quickly. This test is still + * useful on a platform without working Robot code or to test a key that a Robot + * can't reach e.g. a key that isn't assigned a KeyCode because it generates a + * character with a diacritic. + */ + +public class KeyCharacterCombinationTest extends Application { + private final TextArea typingArea = new TextArea(""); + private KeyEvent lastPressed = null; + + public static void main(String[] args) { + Application.launch(KeyCharacterCombinationTest.class, args); + } + + @Override + public void start(Stage stage) { + typingArea.setEditable(false); + typingArea.appendText("Press keys that generate printable characters.\n"); + typingArea.appendText("Shifted punctuation keys are most likely to fail.\n\n"); + + typingArea.addEventFilter(KeyEvent.KEY_PRESSED, this::pressedEvent); + typingArea.addEventFilter(KeyEvent.KEY_RELEASED, this::releasedEvent); + typingArea.addEventFilter(KeyEvent.KEY_TYPED, this::typedEvent); + + Scene scene = new Scene(typingArea, 640, 640); + stage.setScene(scene); + stage.setTitle("Key Character Combinations"); + stage.show(); + + Platform.runLater(typingArea::requestFocus); + } + + // Helper Methods for Event Handling + private void passed(String str) { + typingArea.appendText("Passed: " + str + "\n"); + } + + private void failed(String str) { + typingArea.appendText("* Failed: " + str + "\n"); + } + + private void ignored(String str) { + typingArea.appendText("Ignored: " + str + "\n"); + } + + private void pressedEvent(KeyEvent e) { + lastPressed = e; + } + + private void releasedEvent(KeyEvent e) { + lastPressed = null; + } + + private KeyCombination.ModifierValue toModifier(boolean down) + { + if (down) + return KeyCombination.ModifierValue.DOWN; + return KeyCombination.ModifierValue.UP; + } + + private void typedEvent(KeyEvent e) { + if (lastPressed == null) + return; + + // KeyCharacterCombinations only deal with one char at a time. + if (e.getCharacter().length() == 0) { + ignored("no text"); + return; + } + if (e.getCharacter().length() > 1) { + ignored("text too long"); + return; + } + + String keyCodeName = lastPressed.getCode().getName(); + + // Keys that generate control codes (like Tab and Delete) don't + // work on some platforms. There are existing bugs on this which + // will probably never be fixed since these keys should be + // handled using KeyCodeCombinations instead. + if (Character.isISOControl(e.getCharacter().charAt(0))) { + ignored("control key"); + return; + } + + // Construct a KeyCharacterCombination with the same modifiers and verify that it + // matches the key press event. This tests the internal routine + // Toolkit::getKeyCodeForChar. + KeyCombination.ModifierValue shiftModifier = toModifier(lastPressed.isShiftDown()); + KeyCombination.ModifierValue controlModifier = toModifier(lastPressed.isControlDown()); + KeyCombination.ModifierValue altModifier = toModifier(lastPressed.isAltDown()); + KeyCombination.ModifierValue metaModifier = toModifier(lastPressed.isMetaDown()); + KeyCombination.ModifierValue shortcutModifier = toModifier(lastPressed.isShortcutDown()); + + KeyCharacterCombination combination = new KeyCharacterCombination(e.getCharacter(), + shiftModifier, controlModifier, altModifier, metaModifier, shortcutModifier); + + String combinationDescription = combination.getDisplayText(); + if (lastPressed.getCode().isWhitespaceKey()) + { + // Replace 'invisible' characters with their names. + if (!combinationDescription.isEmpty()) + combinationDescription = combinationDescription.substring(0, combinationDescription.length() - 1); + combinationDescription += lastPressed.getCode().getName(); + } + + if (combination.match(lastPressed)) + passed("key code " + keyCodeName + " matched " + combinationDescription); + else + failed("key code " + keyCodeName + " did not match " + combinationDescription); + } +}