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 final Logger LOGGER = LogManager.getLogger(Main.class);
|
||||||
private static MainWindow mainWindow;
|
private static MainWindow mainWindow;
|
||||||
|
private static AutoSquealConfig config;
|
||||||
|
|
||||||
/** Whether the hook and its listener are registered, so retries don't add duplicates. */
|
/** Whether the hook and its listener are registered, so retries don't add duplicates. */
|
||||||
private static boolean hookRegistered = false;
|
private static boolean hookRegistered = false;
|
||||||
@@ -23,7 +24,7 @@ public class Main {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
LOGGER.info("Hello world!");
|
LOGGER.info("Hello world!");
|
||||||
|
|
||||||
AutoSquealConfig config = new ConfigStore().load();
|
config = new ConfigStore().load();
|
||||||
mainWindow = new MainWindow(config);
|
mainWindow = new MainWindow(config);
|
||||||
|
|
||||||
// ⌘Q on macOS, the IDE stop button and signals all bypass
|
// ⌘Q on macOS, the IDE stop button and signals all bypass
|
||||||
@@ -58,7 +59,8 @@ public class Main {
|
|||||||
LOGGER.info("Registering jnativehook library...");
|
LOGGER.info("Registering jnativehook library...");
|
||||||
try {
|
try {
|
||||||
GlobalScreen.registerNativeHook();
|
GlobalScreen.registerNativeHook();
|
||||||
GlobalScreen.addNativeKeyListener(new KeyPressListener());
|
GlobalScreen.addNativeKeyListener(
|
||||||
|
new KeyPressListener(mainWindow::toggleRunning, mainWindow::notifyKeyboardActivity, config));
|
||||||
hookRegistered = true;
|
hookRegistered = true;
|
||||||
LOGGER.info("Successfully registered jnativehook library!");
|
LOGGER.info("Successfully registered jnativehook library!");
|
||||||
return true;
|
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 com.github.kwhat.jnativehook.keyboard.NativeKeyListener;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import wtf.beatrice.autosqueal.Main;
|
import wtf.beatrice.autosqueal.config.AutoSquealConfig;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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 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<>();
|
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
|
@Override
|
||||||
public void nativeKeyPressed(NativeKeyEvent e) {
|
public void nativeKeyPressed(NativeKeyEvent e) {
|
||||||
// the OS fires repeated "press" events while a key is being held down; ignore them,
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// pressed or released while ctrl+alt happen to be held down
|
||||||
if ((e.getKeyCode() == NativeKeyEvent.VC_CONTROL && pressedKeys.contains(NativeKeyEvent.VC_ALT))
|
if ((e.getKeyCode() == NativeKeyEvent.VC_CONTROL && pressedKeys.contains(NativeKeyEvent.VC_ALT))
|
||||||
|| (e.getKeyCode() == NativeKeyEvent.VC_ALT && pressedKeys.contains(NativeKeyEvent.VC_CONTROL))) {
|
|| (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_CONTROL),
|
||||||
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_ALT));
|
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