From 6eeffa52a9548b778eaec6d84419ed30fa555ae3 Mon Sep 17 00:00:00 2001 From: Matteo Hausner Date: Wed, 15 May 2024 21:23:54 +0200 Subject: [PATCH] Fixes #26 - Linux: Lock keys do not work as expected on XWayland Read lock key state via sysfs and set it via UINPUT, to no longer depend on Xkb functions. --- .../controllerbuddy/input/LockKey.java | 16 ++- .../runmode/OutputRunMode.java | 116 ++++++++++-------- .../runmode/X11WithLockKeyFunctions.java | 41 ------- src/main/resources/strings.properties | 2 + src/main/resources/strings_de_DE.properties | 2 + 5 files changed, 77 insertions(+), 100 deletions(-) delete mode 100644 src/main/java/de/bwravencl/controllerbuddy/runmode/X11WithLockKeyFunctions.java diff --git a/src/main/java/de/bwravencl/controllerbuddy/input/LockKey.java b/src/main/java/de/bwravencl/controllerbuddy/input/LockKey.java index 5a1415da..59adc705 100644 --- a/src/main/java/de/bwravencl/controllerbuddy/input/LockKey.java +++ b/src/main/java/de/bwravencl/controllerbuddy/input/LockKey.java @@ -16,29 +16,27 @@ package de.bwravencl.controllerbuddy.input; -import com.sun.jna.platform.unix.X11; -import com.sun.jna.platform.unix.X11.KeySym; -import de.bwravencl.controllerbuddy.runmode.X11WithLockKeyFunctions; import java.awt.event.KeyEvent; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import uk.co.bithatch.linuxio.EventCode; -public record LockKey(String name, int virtualKeyCode, int mask, KeySym keySym) { +public record LockKey(String name, int virtualKeyCode, EventCode eventCode, String sysfsLedName) { public static final String LOCK_SUFFIX = " Lock"; public static final String CAPS_LOCK = "Caps" + LOCK_SUFFIX; - public static final LockKey CapsLockLockKey = new LockKey(CAPS_LOCK, KeyEvent.VK_CAPS_LOCK, - X11WithLockKeyFunctions.STATE_CAPS_LOCK_MASK, new KeySym(X11.XK_CapsLock)); + public static final LockKey CapsLockLockKey = new LockKey(CAPS_LOCK, KeyEvent.VK_CAPS_LOCK, EventCode.KEY_CAPSLOCK, + "capslock"); public static final String NUM_LOCK = "Num" + LOCK_SUFFIX; - public static final LockKey NumLockLockKey = new LockKey(NUM_LOCK, KeyEvent.VK_NUM_LOCK, - X11WithLockKeyFunctions.STATE_NUM_LOCK_MASK, new KeySym(X11WithLockKeyFunctions.XK_NumLock)); + public static final LockKey NumLockLockKey = new LockKey(NUM_LOCK, KeyEvent.VK_NUM_LOCK, EventCode.KEY_NUMLOCK, + "numlock"); public static final Map nameToLockKeyMap; public static final Map virtualKeyCodeToLockKeyMap; private static final String SCROLL_LOCK = "Scroll" + LOCK_SUFFIX; public static final LockKey ScrollLockLockKey = new LockKey(SCROLL_LOCK, KeyEvent.VK_SCROLL_LOCK, - X11WithLockKeyFunctions.STATE_SCROLL_LOCK_MASK, new KeySym(X11WithLockKeyFunctions.XK_ScrollLock)); + EventCode.KEY_SCROLLLOCK, "scrolllock"); public static final List LOCK_KEYS = List.of(CapsLockLockKey, NumLockLockKey, ScrollLockLockKey); static { diff --git a/src/main/java/de/bwravencl/controllerbuddy/runmode/OutputRunMode.java b/src/main/java/de/bwravencl/controllerbuddy/runmode/OutputRunMode.java index 2213eec8..9d50583a 100644 --- a/src/main/java/de/bwravencl/controllerbuddy/runmode/OutputRunMode.java +++ b/src/main/java/de/bwravencl/controllerbuddy/runmode/OutputRunMode.java @@ -17,11 +17,8 @@ package de.bwravencl.controllerbuddy.runmode; import com.sun.jna.IntegerType; -import com.sun.jna.Memory; import com.sun.jna.Native; import com.sun.jna.Platform; -import com.sun.jna.Pointer; -import com.sun.jna.platform.unix.X11; import com.sun.jna.platform.win32.Advapi32Util; import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.User32; @@ -51,17 +48,21 @@ import java.awt.EventQueue; import java.awt.Toolkit; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashSet; -import java.util.Objects; +import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.swing.JOptionPane; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; @@ -109,6 +110,10 @@ public abstract class OutputRunMode extends RunMode { EventCode.BTN_TRIGGER_HAPPY33, EventCode.BTN_TRIGGER_HAPPY34, EventCode.BTN_TRIGGER_HAPPY35, EventCode.BTN_TRIGGER_HAPPY36, EventCode.BTN_TRIGGER_HAPPY37, EventCode.BTN_TRIGGER_HAPPY38, EventCode.BTN_TRIGGER_HAPPY39, EventCode.BTN_TRIGGER_HAPPY40 }; + private static final String SYSFS_LEDS_DIR = File.separator + "sys" + File.separator + "class" + File.separator + + "leds"; + private static final String SYSFS_INPUT_DIR_REGEX_PREFIX = "input\\d+::"; + private static final String SYSFS_BRIGHTNESS_FILENAME = "brightness"; private static VjoyInterface vJoy; final Set oldDownMouseButtons = new HashSet<>(); final Set newUpMouseButtons = new HashSet<>(); @@ -145,6 +150,7 @@ public abstract class OutputRunMode extends RunMode { private DBusConnection dBusConnection; private ScreenSaver screenSaver; private UInt32 screenSaverCookie; + private Map lockKeyToBrightnessFileMap; OutputRunMode(final Main main, final Input input) { super(main, input); @@ -184,52 +190,6 @@ public static String getVJoyArchFolderName() { } } - private static void setLockKeyState(final LockKey lockKey, final boolean on) { - if (Main.isWindows) { - final var virtualKeyCode = lockKey.virtualKeyCode(); - - final var state = (User32WithGetKeyState.INSTANCE.GetKeyState(virtualKeyCode) & 0x1) != 0; - if (state != on) { - final var toolkit = Toolkit.getDefaultToolkit(); - - toolkit.setLockingKeyState(virtualKeyCode, true); - toolkit.setLockingKeyState(virtualKeyCode, false); - } - } else if (Main.isLinux) { - final var display = X11.INSTANCE.XOpenDisplay(null); - if (Objects.equals(display.getPointer(), Pointer.NULL)) { - throw new RuntimeException("XOpenDisplay() unsucessful"); - } - - try { - final var state_return = new Memory(Integer.SIZE); - if (X11WithLockKeyFunctions.INSTANCE.XkbGetIndicatorState(display, - X11WithLockKeyFunctions.XkbUseCoreKbd, state_return) != X11.Success) { - throw new RuntimeException("XkbGetIndicatorState() unsucessful"); - } - - final var state = (state_return.getInt(0L) & lockKey.mask()) != 0; - if (state != on) { - final var modifierMask = X11WithLockKeyFunctions.INSTANCE.XkbKeysymToModifiers(display, - lockKey.keySym()); - if (modifierMask == 0) { - log.log(Level.WARNING, lockKey + " key is not supported on this system"); - return; - } - - if (!X11WithLockKeyFunctions.INSTANCE.XkbLockModifiers(display, - X11WithLockKeyFunctions.XkbUseCoreKbd, modifierMask, on ? modifierMask : 0)) { - throw new RuntimeException("XkbLockModifiers() unsucessful"); - } - } - } finally { - X11.INSTANCE.XCloseDisplay(display); - } - } else { - throw new UnsupportedOperationException(); - } - } - private static void setVJoy(final VjoyInterface vJoy) { OutputRunMode.vJoy = vJoy; } @@ -642,6 +602,35 @@ final boolean init() { Main.strings.getString("ERROR_DIALOG_TITLE"), JOptionPane.ERROR_MESSAGE)); return false; } + + try { + lockKeyToBrightnessFileMap = LockKey.LOCK_KEYS.stream() + .collect(Collectors.toUnmodifiableMap(lockKey -> lockKey, lockKey -> { + try (final var filesStream = Files.list(Path.of(SYSFS_LEDS_DIR))) { + final var brightnessFile = filesStream.sorted().filter(p -> { + final var fileName = p.getFileName(); + return fileName != null && fileName.toString() + .matches(SYSFS_INPUT_DIR_REGEX_PREFIX + lockKey.sysfsLedName()); + }).findFirst().orElseThrow(() -> new RuntimeException(lockKey.sysfsLedName())) + .resolve(SYSFS_BRIGHTNESS_FILENAME).toFile(); + + if (!brightnessFile.isFile() || !brightnessFile.canRead()) { + throw new IOException("Unable to read: " + brightnessFile); + } + + return brightnessFile; + } catch (final IOException e) { + throw new RuntimeException(e); + } + })); + } catch (final Throwable t) { + log.log(Level.WARNING, t.getMessage(), t); + + EventQueue.invokeLater(() -> GuiUtils.showMessageDialog(main, main.getFrame(), + Main.strings.getString("CANNOT_READ_LED_STATUS_DIALOG_TEXT"), + Main.strings.getString("ERROR_DIALOG_TITLE"), JOptionPane.ERROR_MESSAGE)); + return false; + } } else { throw new UnsupportedOperationException(); } @@ -670,6 +659,33 @@ boolean readInput() throws IOException { return true; } + private void setLockKeyState(final LockKey lockKey, final boolean on) throws IOException { + if (Main.isWindows) { + final var virtualKeyCode = lockKey.virtualKeyCode(); + + final var state = (User32WithGetKeyState.INSTANCE.GetKeyState(virtualKeyCode) & 0x1) != 0; + if (state != on) { + final var toolkit = Toolkit.getDefaultToolkit(); + + toolkit.setLockingKeyState(virtualKeyCode, true); + toolkit.setLockingKeyState(virtualKeyCode, false); + } + } else if (Main.isLinux) { + final var brightnessFile = lockKeyToBrightnessFileMap.get(lockKey); + + try (final var fileInputStream = new FileInputStream(brightnessFile)) { + final var ledState = fileInputStream.read(); + + if (ledState != (on ? '1' : '0')) { + keyboardInputDevice.emit(new Event(lockKey.eventCode(), 1)); + keyboardInputDevice.emit(new Event(lockKey.eventCode(), 0)); + } + } + } else { + throw new UnsupportedOperationException(); + } + } + @Override void setnButtons(final int nButtons) { super.setnButtons(nButtons); diff --git a/src/main/java/de/bwravencl/controllerbuddy/runmode/X11WithLockKeyFunctions.java b/src/main/java/de/bwravencl/controllerbuddy/runmode/X11WithLockKeyFunctions.java deleted file mode 100644 index f5046e88..00000000 --- a/src/main/java/de/bwravencl/controllerbuddy/runmode/X11WithLockKeyFunctions.java +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright (C) 2022 Matteo Hausner - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package de.bwravencl.controllerbuddy.runmode; - -import com.sun.jna.Native; -import com.sun.jna.Pointer; -import com.sun.jna.platform.unix.X11; - -public interface X11WithLockKeyFunctions extends X11 { - - int XkbUseCoreKbd = 0x0100; - - int STATE_CAPS_LOCK_MASK = 0x0001; - int STATE_NUM_LOCK_MASK = 0x0002; - int STATE_SCROLL_LOCK_MASK = 0x0004; - - int XK_NumLock = 0xFF7F; - int XK_ScrollLock = 0xFF14; - - X11WithLockKeyFunctions INSTANCE = Native.load("X11", X11WithLockKeyFunctions.class); - - int XkbGetIndicatorState(Display display, int device_spec, Pointer state_return); - - int XkbKeysymToModifiers(Display dpy, KeySym ks); - - boolean XkbLockModifiers(Display display, int device_spec, int affect, int values); -} diff --git a/src/main/resources/strings.properties b/src/main/resources/strings.properties index ab9da3d7..3d377326 100644 --- a/src/main/resources/strings.properties +++ b/src/main/resources/strings.properties @@ -230,6 +230,8 @@ COULD_NOT_ACQUIRE_VJOY_DEVICE_DIALOG_TEXT = Could not acquire vJoy device {0,num COULD_NOT_OPEN_UINPUT_DEVICE_DIALOG_TEXT = Could not open an uinput device!\n\nUnable to continue. +CANNOT_READ_LED_STATUS_DIALOG_TEXT = Cannot access LED status information via sysfs!\n\nUnable to continue. + COULD_NOT_RESET_VJOY_DEVICE_DIALOG_TEXT = Could not reset vJoy device {0,number,integer}!\n\nUnable to continue. TOO_FEW_VJOY_BUTTONS_DIALOG_TEXT = Too few vJoy buttons!\n\nvJoy device {0,number,integer} has {1,number,integer} buttons.\nThe current requires at least {2,number,integer} buttons.\nPlease increase the number of buttons accordingly using the 'Configure vJoy' application. diff --git a/src/main/resources/strings_de_DE.properties b/src/main/resources/strings_de_DE.properties index fa2f7ad9..72ebd4c4 100644 --- a/src/main/resources/strings_de_DE.properties +++ b/src/main/resources/strings_de_DE.properties @@ -206,6 +206,8 @@ COULD_NOT_ACQUIRE_VJOY_DEVICE_DIALOG_TEXT = vJoy Gerät {0,number,integer} konnt COULD_NOT_OPEN_UINPUT_DEVICE_DIALOG_TEXT = Ein uinput Gerät konnte nicht geöffnet werden!\n\nEs kann nicht fortgefahren werden. +CANNOT_READ_LED_STATUS_DIALOG_TEXT = Kein Zugriff auf LED-Statusinformationen über sysfs möglich!\n\nEs kann nicht fortgefahren werden. + COULD_NOT_RESET_VJOY_DEVICE_DIALOG_TEXT = vJoy Gerät {0,number,integer} konnte nicht zurückgesetzt werden!\n\nEs kann nicht fortgefahren werden. TOO_FEW_VJOY_BUTTONS_DIALOG_TEXT = Zu wenige vJoy Tasten!\n\nvJoy Gerät {0,number,integer} besitzt {1,number,integer} Tasten. Das aktuelle Profil benötigt mindestens {2,number,integer} Tasten.\nBitte erhöhen Sie die Anzahl der Tasten mithilfe der "Configure vJoy" Applikation entsprechend.