decouple the key listener from the main window
This commit is contained in:
@@ -16,6 +16,7 @@ public class Main {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Main.class);
|
||||
private static MainWindow mainWindow;
|
||||
private static AutoSquealConfig config;
|
||||
|
||||
/** Whether the hook and its listener are registered, so retries don't add duplicates. */
|
||||
private static boolean hookRegistered = false;
|
||||
@@ -23,7 +24,7 @@ public class Main {
|
||||
public static void main(String[] args) {
|
||||
LOGGER.info("Hello world!");
|
||||
|
||||
AutoSquealConfig config = new ConfigStore().load();
|
||||
config = new ConfigStore().load();
|
||||
mainWindow = new MainWindow(config);
|
||||
|
||||
// ⌘Q on macOS, the IDE stop button and signals all bypass
|
||||
@@ -58,7 +59,8 @@ public class Main {
|
||||
LOGGER.info("Registering jnativehook library...");
|
||||
try {
|
||||
GlobalScreen.registerNativeHook();
|
||||
GlobalScreen.addNativeKeyListener(new KeyPressListener());
|
||||
GlobalScreen.addNativeKeyListener(
|
||||
new KeyPressListener(mainWindow::toggleRunning, mainWindow::notifyKeyboardActivity, config));
|
||||
hookRegistered = true;
|
||||
LOGGER.info("Successfully registered jnativehook library!");
|
||||
return true;
|
||||
@@ -89,8 +91,4 @@ public class Main {
|
||||
}
|
||||
}
|
||||
|
||||
public static MainWindow getMainWindow() {
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,16 +4,34 @@ import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent;
|
||||
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import wtf.beatrice.autosqueal.Main;
|
||||
import wtf.beatrice.autosqueal.config.AutoSquealConfig;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class KeyPressListener implements NativeKeyListener
|
||||
{
|
||||
/**
|
||||
* Listens for the global toggle hotkey, and reports every key press to the
|
||||
* away detection: a user who is typing is present, hotkey or not.
|
||||
*
|
||||
* The listener is fully decoupled: it receives the actions to run instead
|
||||
* of reaching into the main window, so it can be wired — and tested — on
|
||||
* its own.
|
||||
*/
|
||||
public class KeyPressListener implements NativeKeyListener {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(KeyPressListener.class);
|
||||
|
||||
private final Runnable toggleAction;
|
||||
private final Runnable keyboardActivityReporter;
|
||||
private final AutoSquealConfig config;
|
||||
private final List<Integer> pressedKeys = new ArrayList<>();
|
||||
|
||||
public KeyPressListener(Runnable toggleAction, Runnable keyboardActivityReporter, AutoSquealConfig config) {
|
||||
this.toggleAction = toggleAction;
|
||||
this.keyboardActivityReporter = keyboardActivityReporter;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nativeKeyPressed(NativeKeyEvent e) {
|
||||
// the OS fires repeated "press" events while a key is being held down; ignore them,
|
||||
@@ -27,18 +45,22 @@ public class KeyPressListener implements NativeKeyListener
|
||||
|
||||
// any key the user presses means they are present: let the away
|
||||
// detection know, so the app doesn't grab the mouse while they type
|
||||
Main.getMainWindow().notifyKeyboardActivity();
|
||||
keyboardActivityReporter.run();
|
||||
|
||||
if (!config.isHotkeyEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// toggle only when the second key of the combo is pressed down: not only does this
|
||||
// fire exactly once per combo, but it also avoids toggling when any other key is
|
||||
// pressed or released while ctrl+alt happen to be held down
|
||||
if ((e.getKeyCode() == NativeKeyEvent.VC_CONTROL && pressedKeys.contains(NativeKeyEvent.VC_ALT))
|
||||
|| (e.getKeyCode() == NativeKeyEvent.VC_ALT && pressedKeys.contains(NativeKeyEvent.VC_CONTROL))) {
|
||||
LOGGER.warn("Received shutdown keystroke: [{}][{}]",
|
||||
LOGGER.info("Received toggle hotkey: [{}][{}]",
|
||||
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_CONTROL),
|
||||
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_ALT));
|
||||
|
||||
Main.getMainWindow().toggleRunning();
|
||||
toggleAction.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package wtf.beatrice.autosqueal.listener;
|
||||
|
||||
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import wtf.beatrice.autosqueal.config.AutoSquealConfig;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class KeyPressListenerTest
|
||||
{
|
||||
private final List<String> events = new ArrayList<>();
|
||||
private final AutoSquealConfig config = new AutoSquealConfig();
|
||||
private final KeyPressListener listener = new KeyPressListener(
|
||||
() -> events.add("toggle"), () -> events.add("activity"), config);
|
||||
|
||||
private static NativeKeyEvent press(int keyCode) {
|
||||
return new NativeKeyEvent(NativeKeyEvent.NATIVE_KEY_PRESSED, 0, 0, keyCode, ' ');
|
||||
}
|
||||
|
||||
private static NativeKeyEvent release(int keyCode) {
|
||||
return new NativeKeyEvent(NativeKeyEvent.NATIVE_KEY_RELEASED, 0, 0, keyCode, ' ');
|
||||
}
|
||||
|
||||
@Test
|
||||
void theComboTogglesOnceOnItsSecondKey() {
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
|
||||
|
||||
// ctrl alone is not the combo: it only reports presence
|
||||
assertEquals(List.of("activity"), events);
|
||||
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
|
||||
|
||||
// the second key completes the combo: presence first, then the toggle
|
||||
assertEquals(List.of("activity", "activity", "toggle"), events);
|
||||
}
|
||||
|
||||
@Test
|
||||
void theComboWorksInTheOtherOrderToo() {
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
|
||||
|
||||
assertEquals(List.of("activity", "activity", "toggle"), events);
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedPressesAreIgnored() {
|
||||
// the OS repeats press events while a key is held down: they must
|
||||
// not toggle repeatedly
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
|
||||
|
||||
events.clear();
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
|
||||
|
||||
assertEquals(List.of(), events);
|
||||
}
|
||||
|
||||
@Test
|
||||
void theComboCanFireAgainAfterBothKeysAreReleased() {
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
|
||||
listener.nativeKeyReleased(release(NativeKeyEvent.VC_CONTROL));
|
||||
listener.nativeKeyReleased(release(NativeKeyEvent.VC_ALT));
|
||||
|
||||
events.clear();
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
|
||||
|
||||
assertEquals(List.of("activity", "activity", "toggle"), events);
|
||||
}
|
||||
|
||||
@Test
|
||||
void theHotkeyCanBeDisabledWithoutLosingPresenceReporting() {
|
||||
config.setHotkeyEnabled(false);
|
||||
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
|
||||
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
|
||||
|
||||
// typing still counts as presence, but the combo does nothing
|
||||
assertEquals(List.of("activity", "activity"), events);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user