Skip to content

Commit

Permalink
Fixes #26 - Linux: Lock keys do not work as expected on XWayland
Browse files Browse the repository at this point in the history
Read lock key state via sysfs and set it via UINPUT, to no longer depend
on Xkb functions.
  • Loading branch information
bwRavencl committed May 15, 2024
1 parent 23df526 commit 6eeffa5
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 100 deletions.
16 changes: 7 additions & 9 deletions src/main/java/de/bwravencl/controllerbuddy/input/LockKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, LockKey> nameToLockKeyMap;
public static final Map<Integer, LockKey> 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<LockKey> LOCK_KEYS = List.of(CapsLockLockKey, NumLockLockKey, ScrollLockLockKey);

static {
Expand Down
116 changes: 66 additions & 50 deletions src/main/java/de/bwravencl/controllerbuddy/runmode/OutputRunMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<Integer> oldDownMouseButtons = new HashSet<>();
final Set<Integer> newUpMouseButtons = new HashSet<>();
Expand Down Expand Up @@ -145,6 +150,7 @@ public abstract class OutputRunMode extends RunMode {
private DBusConnection dBusConnection;
private ScreenSaver screenSaver;
private UInt32 screenSaverCookie;
private Map<LockKey, File> lockKeyToBrightnessFileMap;

OutputRunMode(final Main main, final Input input) {
super(main, input);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down

This file was deleted.

2 changes: 2 additions & 0 deletions src/main/resources/strings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/strings_de_DE.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 6eeffa5

Please sign in to comment.