fix scheduling, threading and movement bugs
scheduling: - cursor mover created a new java.util.timer for every movement and never cancelled it, leaking a live thread roughly every 10 seconds; timers are replaced by a single shared scheduledexecutorservice with daemon threads, shut down when the automation stops. clicks: - the double click sequence used thread.sleep() inside a timer task, blocking the scheduler thread; it is now scheduled as one-shot actions on the shared scheduler, with the same pacing as before. - stopping the automation now also cancels in-flight movements and pending clicks, instead of letting them run to completion. movement math: - when the destination shared one coordinate with the starting position, the step on the other axis was computed as length/0 = infinity, and the stop condition checked one axis with || instead of &&, so a movement could stop (and click) before reaching its destination. steps are now guarded and the movement stops only when both axes have converged. ui and lifecycle: - the app could not be closed: closing the window only hid it, and ESC unregistered the hotkey without exiting. the window now exits cleanly on close, stopping the automation and unregistering the native hook. - toggleRunning() was called from the jnativehook thread and touched swing components off the EDT; UI updates are now marshalled to the EDT. - the hotkey thrashed: key repeats re-fired the toggle while ctrl+alt was held, and any other key pressed with the combo held toggled it too. it now fires exactly once, when the combo is completed. away-detection (CursorMoveListener) stays intentionally unscheduled: it cannot tell the user's movements apart from the app's own, and will be reworked in a future iteration.
This commit is contained in:
@@ -3,15 +3,19 @@ package wtf.beatrice.autosqueal.ui;
|
||||
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import wtf.beatrice.autosqueal.Main;
|
||||
import wtf.beatrice.autosqueal.controls.CursorMover;
|
||||
import wtf.beatrice.autosqueal.listener.CursorMoveListener;
|
||||
import wtf.beatrice.autosqueal.util.RunnerUtil;
|
||||
import wtf.beatrice.autosqueal.util.SystemUtil;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Timer;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MainWindow
|
||||
{
|
||||
@@ -20,22 +24,37 @@ public class MainWindow
|
||||
private static final int WINDOW_HEIGHT = 700;
|
||||
private static final int WINDOW_WIDTH = 800;
|
||||
|
||||
private Timer timerRunner = new Timer();
|
||||
private CursorMover cursorMover = new CursorMover();
|
||||
private static final long AUTOMATION_START_DELAY_SECONDS = 1L;
|
||||
|
||||
private final JFrame frame = new JFrame();
|
||||
private Button toggleButton;
|
||||
|
||||
private ScheduledExecutorService scheduler;
|
||||
private CursorMover cursorMover;
|
||||
|
||||
/**
|
||||
* Builds and shows the main window, and starts the automation.
|
||||
* Must be called on the EDT.
|
||||
*/
|
||||
public void init() {
|
||||
|
||||
JFrame frame = new JFrame();
|
||||
frame.setSize(new Dimension(WINDOW_WIDTH, WINDOW_HEIGHT));
|
||||
frame.setTitle("autosqueal");
|
||||
frame.setResizable(false);
|
||||
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
LOGGER.info("Shutting down...");
|
||||
stopAutomation();
|
||||
Main.unregisterJNativeHook();
|
||||
}
|
||||
});
|
||||
|
||||
toggleButton = new Button();
|
||||
toggleButton.setBounds(new Rectangle((WINDOW_WIDTH / 2) - 60, WINDOW_HEIGHT - 60, 120, 30));
|
||||
toggleButton.addActionListener(e -> toggleRunning());
|
||||
frame.add(toggleButton);
|
||||
toggleRunning();
|
||||
|
||||
int bordersPx = 10;
|
||||
int rescaleRateo = ((WINDOW_WIDTH - (2 * bordersPx)) * 100) / RunnerUtil.SCREEN_WIDTH;
|
||||
@@ -50,9 +69,11 @@ public class MainWindow
|
||||
timestampLabel.setBounds(new Rectangle(bordersPx, bordersPx + rescaleHeight + bordersPx, 100, 30));
|
||||
frame.add(timestampLabel);
|
||||
|
||||
|
||||
frame.setLayout(null);
|
||||
frame.setVisible(true);
|
||||
|
||||
startAutomation();
|
||||
updateToggleLabel();
|
||||
}
|
||||
|
||||
private Image getScreenCapture(int rescaleWidth, int rescaleHeight) {
|
||||
@@ -100,38 +121,61 @@ public class MainWindow
|
||||
return bimage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the automation on or off. Safe to call from any thread: the actual
|
||||
* work is always marshalled to the EDT, since it touches Swing components.
|
||||
*/
|
||||
public void toggleRunning() {
|
||||
|
||||
String label;
|
||||
|
||||
if (cursorMover == null) {
|
||||
timerRunner = new Timer();
|
||||
CursorMoveListener cursorMoveListener = new CursorMoveListener();
|
||||
timerRunner.schedule(cursorMoveListener, 0L, 1000L);
|
||||
|
||||
cursorMover = new CursorMover();
|
||||
timerRunner.schedule(cursorMover, 1000L, RunnerUtil.SECONDS_BETWEEN_MOVES * 1000L);
|
||||
|
||||
label = "Stop [" +
|
||||
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_CONTROL) +
|
||||
"][" +
|
||||
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_ALT) +
|
||||
"]";
|
||||
}
|
||||
else {
|
||||
timerRunner.cancel();
|
||||
|
||||
cursorMover = null;
|
||||
|
||||
label = "Start [" +
|
||||
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_CONTROL) +
|
||||
"][" +
|
||||
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_ALT) +
|
||||
"]";
|
||||
}
|
||||
|
||||
toggleButton.setLabel(label);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (cursorMover == null) {
|
||||
startAutomation();
|
||||
} else {
|
||||
stopAutomation();
|
||||
}
|
||||
updateToggleLabel();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
private void startAutomation() {
|
||||
LOGGER.info("Starting automation");
|
||||
|
||||
scheduler = Executors.newScheduledThreadPool(3, runnable -> {
|
||||
Thread thread = new Thread(runnable, "autosqueal-scheduler");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
cursorMover = new CursorMover(scheduler);
|
||||
|
||||
// note: away-detection (CursorMoveListener) is intentionally not scheduled here.
|
||||
// as it is, it cannot tell the user's movements apart from the movements the app
|
||||
// itself makes, so it would always think the user is present. it will be reworked
|
||||
// and re-enabled in a future iteration.
|
||||
scheduler.scheduleWithFixedDelay(cursorMover,
|
||||
AUTOMATION_START_DELAY_SECONDS,
|
||||
RunnerUtil.SECONDS_BETWEEN_MOVES,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void stopAutomation() {
|
||||
LOGGER.info("Stopping automation");
|
||||
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdownNow();
|
||||
scheduler = null;
|
||||
}
|
||||
cursorMover = null;
|
||||
}
|
||||
|
||||
private void updateToggleLabel() {
|
||||
if (toggleButton == null) {
|
||||
// the window is not initialized yet
|
||||
return;
|
||||
}
|
||||
|
||||
String hotkey = "[" + NativeKeyEvent.getKeyText(NativeKeyEvent.VC_CONTROL) + "]"
|
||||
+ "[" + NativeKeyEvent.getKeyText(NativeKeyEvent.VC_ALT) + "]";
|
||||
|
||||
toggleButton.setLabel((cursorMover == null ? "Start " : "Stop ") + hotkey);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user