diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/events/KeyEvent.java b/modules/javafx.graphics/src/main/java/com/sun/glass/events/KeyEvent.java index d48ac31c09d..6529aa09ed9 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/events/KeyEvent.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/events/KeyEvent.java @@ -68,6 +68,13 @@ public class KeyEvent { @Native public final static int MODIFIER_BUTTON_BACK = 1 << 8; @Native public final static int MODIFIER_BUTTON_FORWARD = 1 << 9; + /* + * Key lock state + */ + @Native public static final int KEY_LOCK_OFF = 0; + @Native public static final int KEY_LOCK_ON = 1; + @Native public static final int KEY_LOCK_UNKNOWN = -1; + /* * Key event key codes. */ 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 86359465325..b0e3b84de8c 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 @@ -36,6 +36,7 @@ import java.util.List; import java.util.Map; import java.util.LinkedList; +import java.util.Optional; public abstract class Application { @@ -744,4 +745,22 @@ public final boolean supportsSystemMenu() { public static int getKeyCodeForChar(char c) { return application._getKeyCodeForChar(c); } + + protected int _isKeyLocked(int keyCode) { + // Overridden in subclasses + return KeyEvent.KEY_LOCK_UNKNOWN; + } + + public final Optional isKeyLocked(int keyCode) { + checkEventThread(); + int lockState = _isKeyLocked(keyCode); + switch (lockState) { + case KeyEvent.KEY_LOCK_OFF: + return Optional.of(false); + case KeyEvent.KEY_LOCK_ON: + return Optional.of(true); + default: + return Optional.empty(); + } + } } 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 0526b62ba4f..e1d1a61fb7b 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 @@ -478,4 +478,7 @@ protected boolean _supportsInputMethods() { @Override protected native int _getKeyCodeForChar(char c); + @Override + protected native int _isKeyLocked(int keyCode); + } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java index 1d3485a3eae..a240e3a0c1c 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java @@ -391,4 +391,7 @@ public String getDataDirectory() { @Override protected native int _getKeyCodeForChar(char c); + + @Override + protected native int _isKeyLocked(int keyCode); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java index b88f3a1e384..c4e6b0d5ac0 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java @@ -358,4 +358,7 @@ public String getDataDirectory() { @Override protected native int _getKeyCodeForChar(char c); + + @Override + protected native int _isKeyLocked(int keyCode); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/DummyToolkit.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/DummyToolkit.java index 66c1cf3463b..e190b191bbd 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/DummyToolkit.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/DummyToolkit.java @@ -67,6 +67,7 @@ import com.sun.scenario.animation.AbstractPrimaryTimer; import com.sun.scenario.effect.FilterContext; import com.sun.scenario.effect.Filterable; +import java.util.Optional; /** * A stubbed out Toolkit that provides no useful implementation. This is used @@ -371,6 +372,11 @@ public KeyCode getPlatformShortcutKey() { throw new UnsupportedOperationException("Not supported yet."); } + @Override + public Optional isKeyLocked(KeyCode keyCode) { + throw new UnsupportedOperationException("Not supported yet."); + } + @Override public FileChooserResult showFileChooser(TKStage ownerWindow, String title, 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 ebdbc629973..829bc9a441e 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 @@ -94,6 +94,7 @@ import com.sun.scenario.effect.Color4f; import com.sun.scenario.effect.FilterContext; import com.sun.scenario.effect.Filterable; +import java.util.Optional; public abstract class Toolkit { @@ -873,6 +874,13 @@ public KeyCode getPlatformShortcutKey() { return PlatformUtil.isMac() ? KeyCode.META : KeyCode.CONTROL; } + /** + * Returns the lock state for the given keyCode. + * @param keyCode the keyCode to check + * @return the lock state for the given keyCode. + */ + public abstract Optional isKeyLocked(KeyCode keyCode); + public abstract FileChooserResult showFileChooser( TKStage ownerWindow, String title, 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 12adff0ee37..08f6f777542 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 @@ -133,6 +133,7 @@ import com.sun.javafx.logging.PulseLogger; import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; import com.sun.javafx.scene.input.DragboardHelper; +import java.util.Optional; public final class QuantumToolkit extends Toolkit { @@ -1236,6 +1237,24 @@ public boolean isMSAASupported() { return GraphicsPipeline.getPipeline().isMSAASupported(); } + // Returns the glass keycode for the given JavaFX KeyCode. + // This method only converts lock state KeyCode values + private int toGlassKeyCode(KeyCode keyCode) { + switch (keyCode) { + case CAPS: + return com.sun.glass.events.KeyEvent.VK_CAPS_LOCK; + case NUM_LOCK: + return com.sun.glass.events.KeyEvent.VK_NUM_LOCK; + default: + return com.sun.glass.events.KeyEvent.VK_UNDEFINED; + } + } + + @Override + public Optional isKeyLocked(KeyCode keyCode) { + return Application.GetApplication().isKeyLocked(toGlassKeyCode(keyCode)); + } + static TransferMode clipboardActionToTransferMode(final int action) { switch (action) { case Clipboard.ACTION_NONE: diff --git a/modules/javafx.graphics/src/main/java/javafx/application/Platform.java b/modules/javafx.graphics/src/main/java/javafx/application/Platform.java index 10ad074c4b9..18d233e083e 100644 --- a/modules/javafx.graphics/src/main/java/javafx/application/Platform.java +++ b/modules/javafx.graphics/src/main/java/javafx/application/Platform.java @@ -25,10 +25,12 @@ package javafx.application; +import com.sun.javafx.application.PlatformImpl; import com.sun.javafx.tk.Toolkit; +import java.util.Optional; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; -import com.sun.javafx.application.PlatformImpl; +import javafx.scene.input.KeyCode; /** * Application platform support class. @@ -329,6 +331,43 @@ public static void exitNestedEventLoop(Object key, Object rval) { Toolkit.getToolkit().exitNestedEventLoop(key, rval); } + /** + * Returns a flag indicating whether the key corresponding to {@code keyCode} + * is in the locked (or "on") state. + * {@code keyCode} must be one of: {@link KeyCode#CAPS} or + * {@link KeyCode#NUM_LOCK}. + * If the underlying system is not able to determine the state of the + * specified {@code keyCode}, an empty {@code Optional} is returned. + * If the keyboard attached to the system doesn't have the specified key, + * an {@code Optional} containing {@code false} is returned. + * This method must be called on the JavaFX Application thread. + * + * @param keyCode the {@code KeyCode} of the lock state to query + * + * @return the lock state of the key corresponding to {@code keyCode}, + * or an empty {@code Optional} if the system cannot determine its state + * + * @throws IllegalArgumentException if {@code keyCode} is not one of the + * valid {@code KeyCode} values + * + * @throws IllegalStateException if this method is called on a thread + * other than the JavaFX Application Thread + * + * @since 17 + */ + public static Optional isKeyLocked(KeyCode keyCode) { + Toolkit.getToolkit().checkFxUserThread(); + + switch (keyCode) { + case CAPS: + case NUM_LOCK: + break; + default: + throw new IllegalArgumentException("Invalid KeyCode"); + } + return Toolkit.getToolkit().isKeyLocked(keyCode); + } + /** * Checks whether a nested event loop is running, returning true to indicate * that one is, and false if there are no nested event loops currently 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 9f1b2691669..554031bbceb 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 @@ -29,6 +29,7 @@ #include #include "glass_general.h" #include +#include static gboolean key_initialized = FALSE; static GHashTable *keymap; @@ -345,4 +346,63 @@ JNIEXPORT jint JNICALL Java_com_sun_glass_ui_gtk_GtkApplication__1getKeyCodeForC return gdk_keyval_to_glass(keyval); } +/* + * Function to determine whether the Xkb extention is available. This is a + * precaution against X protocol errors, although it should be available on all + * Linux systems. + */ + +static Bool xkbInitialized = False; +static Bool xkbAvailable = False; + +static Bool isXkbAvailable(Display *display) { + if (!xkbInitialized) { + int xkbMajor = XkbMajorVersion; + int xkbMinor = XkbMinorVersion; + xkbAvailable = XkbQueryExtension(display, NULL, NULL, NULL, &xkbMajor, &xkbMinor); + xkbInitialized = True; + } + return xkbAvailable; +} + +/* + * Class: com_sun_glass_ui_gtk_GtkApplication + * Method: _isKeyLocked + * Signature: (I)I + */ +JNIEXPORT jint JNICALL Java_com_sun_glass_ui_gtk_GtkApplication__1isKeyLocked + (JNIEnv * env, jobject obj, jint keyCode) +{ + Display* display = gdk_x11_display_get_xdisplay(gdk_display_get_default()); + if (!isXkbAvailable(display)) { + return com_sun_glass_events_KeyEvent_KEY_LOCK_UNKNOWN; + } + + Atom keyCodeAtom = None; + switch (keyCode) { + case com_sun_glass_events_KeyEvent_VK_CAPS_LOCK: + keyCodeAtom = XInternAtom(display, "Caps Lock", True); + break; + + case com_sun_glass_events_KeyEvent_VK_NUM_LOCK: + keyCodeAtom = XInternAtom(display, "Num Lock", True); + break; + } + + if (keyCodeAtom == None) { + return com_sun_glass_events_KeyEvent_KEY_LOCK_UNKNOWN; + } + + Bool isLocked = False; + if (XkbGetNamedIndicator(display, keyCodeAtom, NULL, &isLocked, NULL, NULL)) { + if (isLocked) { + return com_sun_glass_events_KeyEvent_KEY_LOCK_ON; + } else { + return com_sun_glass_events_KeyEvent_KEY_LOCK_OFF; + } + } + + return com_sun_glass_events_KeyEvent_KEY_LOCK_UNKNOWN; +} + } // extern "C" diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassKey.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassKey.m index e0d60871e0a..38a828a41eb 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassKey.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassKey.m @@ -396,3 +396,25 @@ BOOL GetMacKey(jint javaKeyCode, unsigned short *outMacKeyCode) return [GlassApplication getKeyCodeForChar:c]; } +/* + * Class: com_sun_glass_ui_mac_MacApplication + * Method: _isKeyLocked + * Signature: (I)I + */ +JNIEXPORT jint JNICALL Java_com_sun_glass_ui_mac_MacApplication__1isKeyLocked + (JNIEnv * env, jobject obj, jint keyCode) +{ + NSUInteger mask = 0; + switch (keyCode) { + case com_sun_glass_events_KeyEvent_VK_CAPS_LOCK: + mask = NSEventModifierFlagCapsLock; + break; + + // Caps lock is the only locking key supported on macOS + default: + return com_sun_glass_events_KeyEvent_KEY_LOCK_UNKNOWN; + } + NSUInteger modifierFlags = [NSEvent modifierFlags]; + return (modifierFlags & mask) ? com_sun_glass_events_KeyEvent_KEY_LOCK_ON + : com_sun_glass_events_KeyEvent_KEY_LOCK_OFF; +} diff --git a/modules/javafx.graphics/src/main/native-glass/win/KeyTable.cpp b/modules/javafx.graphics/src/main/native-glass/win/KeyTable.cpp index 1d4fba9b342..62338dab063 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/KeyTable.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/KeyTable.cpp @@ -247,5 +247,27 @@ JNIEXPORT jint JNICALL Java_com_sun_glass_ui_win_WinApplication__1getKeyCodeForC return WindowsKeyToJavaKey(vkey); } +/* + * Class: com_sun_glass_ui_win_WinApplication + * Method: _isKeyLocked + * Signature: (I)I + */ +JNIEXPORT jint JNICALL Java_com_sun_glass_ui_win_WinApplication__1isKeyLocked + (JNIEnv * env, jobject obj, jint keyCode) +{ + SHORT keyState = 0; + switch (keyCode) { + case com_sun_glass_events_KeyEvent_VK_CAPS_LOCK: + keyState = ::GetKeyState(VK_CAPITAL); + break; + case com_sun_glass_events_KeyEvent_VK_NUM_LOCK: + keyState = ::GetKeyState(VK_NUMLOCK); + break; + default: + return com_sun_glass_events_KeyEvent_KEY_LOCK_UNKNOWN; + } + return (keyState & 0x1) ? com_sun_glass_events_KeyEvent_KEY_LOCK_ON + : com_sun_glass_events_KeyEvent_KEY_LOCK_OFF; +} diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubToolkit.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubToolkit.java index 15e8f98a130..9c9585d97cc 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubToolkit.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubToolkit.java @@ -750,6 +750,11 @@ public KeyCode getPlatformShortcutKey() { return platformShortcutKey; } + @Override + public Optional isKeyLocked(KeyCode keyCode) { + return Optional.empty(); + } + private DndDelegate dndDelegate; public void setDndDelegate(DndDelegate dndDelegate) { this.dndDelegate = dndDelegate; diff --git a/tests/manual/events/CapsLockTest.java b/tests/manual/events/CapsLockTest.java new file mode 100644 index 00000000000..6414f7beba8 --- /dev/null +++ b/tests/manual/events/CapsLockTest.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.Optional; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.input.KeyCode; +import javafx.stage.Stage; + +public class CapsLockTest { + + private static BufferedReader reader; + + public static class App extends Application { + private void checkCapsLock(boolean expected) throws Exception { + Optional capsLock = Platform.isKeyLocked(KeyCode.CAPS); + if (capsLock.isPresent()) { + System.out.println("isKeyLocked(CAPS) is " + capsLock.get()); + if (capsLock.get() != expected) { + System.out.println("TEST FAILED"); + System.exit(1); + } + } else { + System.out.println("ERROR: isKeyLocked(CAPS) is empty"); + System.out.println("TEST FAILED"); + System.exit(1); + } + } + + @Override + public void start(Stage stage) throws Exception { + checkCapsLock(true); + System.out.println("Disable Caps Lock on your system then press ENTER"); + reader.readLine(); + checkCapsLock(false); + Platform.exit(); + } + + } + + public static void main(String[] args) { + System.out.println("Enable Caps Lock on your system then press ENTER"); + try { + reader = new BufferedReader(new InputStreamReader(System.in)); + reader.readLine(); + Application.launch(App.class, args); + } catch (Exception ex) { + ex.printStackTrace(System.out); + System.out.println("TEST FAILED"); + System.exit(1); + } + System.out.println(); + System.out.println("TEST PASSED"); + } +} diff --git a/tests/system/src/test/java/test/robot/javafx/application/KeyLockedTest.java b/tests/system/src/test/java/test/robot/javafx/application/KeyLockedTest.java new file mode 100644 index 00000000000..d2888946f85 --- /dev/null +++ b/tests/system/src/test/java/test/robot/javafx/application/KeyLockedTest.java @@ -0,0 +1,150 @@ +/* + * 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 test.robot.javafx.application; + +import com.sun.javafx.PlatformUtil; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javafx.application.Platform; +import javafx.scene.input.KeyCode; +import javafx.scene.robot.Robot; +import test.util.Util; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; + +/** + * Test program for Platform::isKeyLocked. + */ +public class KeyLockedTest { + + // Used to start the toolkit before running any test + private static final CountDownLatch startupLatch = new CountDownLatch(1); + private static Robot robot; + + @BeforeClass + public static void initFX() throws Exception { + Platform.setImplicitExit(false); + Platform.startup(startupLatch::countDown); + assertTrue("Timeout waiting for FX runtime to start", + startupLatch.await(15, TimeUnit.SECONDS)); + + if (PlatformUtil.isWindows()) { + Util.runAndWait(() -> robot = new Robot()); + } + } + + @AfterClass + public static void cleanupFX() { + if (robot != null) { + // Disable caps lock if it is set + Platform.runLater(() -> { + Optional capsLockState = Platform.isKeyLocked(KeyCode.CAPS); + capsLockState.ifPresent(state -> { + if (state) { + robot.keyPress(KeyCode.CAPS); + robot.keyRelease(KeyCode.CAPS); + } + }); + }); + } + Platform.exit(); + } + + @Test(expected = IllegalStateException.class) + public void testCallOnTestThread() { + // This should throw an exception + Optional capsLockState = Platform.isKeyLocked(KeyCode.CAPS); + } + + @Test(expected = IllegalArgumentException.class) + public void testIllegalKeyCode() { + Util.runAndWait(() -> { + // This should throw an exception + Optional capsLockState = Platform.isKeyLocked(KeyCode.A); + }); + } + + @Test + public void testCanReadCapsLockState() { + Util.runAndWait(() -> { + // Check that we don't get an exception or a null optional. + Optional capsLockState = Platform.isKeyLocked(KeyCode.CAPS); + assertNotNull(capsLockState); + // A result should always be present + assertTrue(capsLockState.isPresent()); + }); + } + + @Test + public void testCanReadNumLockState() { + Util.runAndWait(() -> { + // Check that we don't get an exception or a null optional. + Optional numLockState = Platform.isKeyLocked(KeyCode.NUM_LOCK); + assertNotNull(numLockState); + // A result should always be present on Windows and Linux + if (PlatformUtil.isWindows() || PlatformUtil.isLinux()) { + assertTrue(numLockState.isPresent()); + } + // A result should never be present on Mac + if (PlatformUtil.isMac()) { + assertFalse(numLockState.isPresent()); + } + }); + } + + @Test + public void testCapsLockState() { + // We can set caps lock via robot only on Windows + assumeTrue(PlatformUtil.isWindows()); + + final AtomicBoolean initialCapsLock = new AtomicBoolean(false); + Util.runAndWait(() -> { + Optional capsLockState = Platform.isKeyLocked(KeyCode.CAPS); + assertNotNull(capsLockState); + assertTrue(capsLockState.isPresent()); + + // Read the initial state of the caps lock key and then toggle it + initialCapsLock.set(capsLockState.get()); + robot.keyPress(KeyCode.CAPS); + robot.keyRelease(KeyCode.CAPS); + }); + // Wait for 1/2 second to make sure the state has toggled + Util.sleep(500); + Util.runAndWait(() -> { + Optional capsLockState = Platform.isKeyLocked(KeyCode.CAPS); + assertNotNull(capsLockState); + assertTrue(capsLockState.isPresent()); + assertTrue(initialCapsLock.get() != capsLockState.get()); + }); + } +}