312 lines
11 KiB
Java
312 lines
11 KiB
Java
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.controls.RobotMouseTracker;
|
|
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.io.IOException;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
public class MainWindow
|
|
{
|
|
private static final Logger LOGGER = LogManager.getLogger(MainWindow.class);
|
|
|
|
private static final int WINDOW_HEIGHT = 700;
|
|
private static final int WINDOW_WIDTH = 800;
|
|
|
|
private static final long AUTOMATION_START_DELAY_SECONDS = 1L;
|
|
private static final long AWAY_POLL_INTERVAL_SECONDS = 1L;
|
|
|
|
private final JFrame frame = new JFrame();
|
|
private JButton toggleButton;
|
|
|
|
private ScheduledExecutorService periodicScheduler;
|
|
private ExecutorService movementExecutor;
|
|
private CursorMover cursorMover;
|
|
|
|
private TrayIcon trayIcon;
|
|
private MenuItem trayToggleItem;
|
|
|
|
/**
|
|
* Builds and shows the main window, and starts the automation.
|
|
* Must be called on the EDT.
|
|
*/
|
|
public void init() {
|
|
|
|
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) {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
toggleButton = new JButton();
|
|
toggleButton.setBounds(new Rectangle((WINDOW_WIDTH / 2) - 60, WINDOW_HEIGHT - 60, 120, 30));
|
|
toggleButton.addActionListener(e -> toggleRunning());
|
|
frame.add(toggleButton);
|
|
|
|
int bordersPx = 10;
|
|
int rescaleRateo = ((WINDOW_WIDTH - (2 * bordersPx)) * 100) / RunnerUtil.SCREEN_WIDTH;
|
|
int rescaleWidth = RunnerUtil.SCREEN_WIDTH * rescaleRateo / 100;
|
|
int rescaleHeight = RunnerUtil.SCREEN_HEIGHT * rescaleRateo / 100;
|
|
JLabel imageLabel = new JLabel(new ImageIcon(getScreenCapture(rescaleWidth, rescaleHeight)));
|
|
imageLabel.setBounds(new Rectangle(bordersPx, bordersPx, rescaleWidth, rescaleHeight));
|
|
frame.add(imageLabel);
|
|
|
|
Image preciseScreenshot = getPreciseScreenshot();
|
|
JLabel timestampLabel = new JLabel(new ImageIcon(preciseScreenshot));
|
|
timestampLabel.setBounds(new Rectangle(bordersPx, bordersPx + rescaleHeight + bordersPx, 100, 30));
|
|
frame.add(timestampLabel);
|
|
|
|
frame.setLayout(null);
|
|
frame.setVisible(true);
|
|
|
|
if (SystemTray.isSupported()) {
|
|
addTrayIcon();
|
|
}
|
|
|
|
startAutomation();
|
|
updateToggleLabel();
|
|
}
|
|
|
|
private Image getScreenCapture(int rescaleWidth, int rescaleHeight) {
|
|
|
|
Image fullImage = getScreenCapture();
|
|
return fullImage.getScaledInstance(rescaleWidth, rescaleHeight, Image.SCALE_FAST);
|
|
}
|
|
|
|
public Image getPreciseScreenshot() {
|
|
|
|
if(SystemUtil.getHostSystem().equals(SystemUtil.OperatingSystem.MAC_OS)) {
|
|
BufferedImage screenshot = getScreenCapture();
|
|
return screenshot.getSubimage(RunnerUtil.SCREEN_WIDTH - 100,0, 100, 30);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public BufferedImage getScreenCapture() {
|
|
try {
|
|
Robot robot = new Robot();
|
|
return robot.createScreenCapture(new Rectangle(RunnerUtil.SCREEN_WIDTH, RunnerUtil.SCREEN_HEIGHT));
|
|
} catch (AWTException e) {
|
|
LOGGER.error("Robot initialization error", e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Adds a menu bar icon with a toggle entry and a quit entry, so the
|
|
* automation can be controlled even with the window closed.
|
|
*/
|
|
private void addTrayIcon() {
|
|
PopupMenu menu = new PopupMenu();
|
|
|
|
trayToggleItem = new MenuItem();
|
|
trayToggleItem.addActionListener(e -> toggleRunning());
|
|
menu.add(trayToggleItem);
|
|
|
|
MenuItem quitItem = new MenuItem("Quit");
|
|
quitItem.addActionListener(e -> {
|
|
cleanup();
|
|
System.exit(0);
|
|
});
|
|
menu.add(quitItem);
|
|
|
|
trayIcon = new TrayIcon(createTrayIconImage(), "autosqueal", menu);
|
|
trayIcon.setImageAutoSize(true);
|
|
|
|
try {
|
|
SystemTray.getSystemTray().add(trayIcon);
|
|
} catch (AWTException ex) {
|
|
LOGGER.error("Could not add the tray icon", ex);
|
|
trayIcon = null;
|
|
trayToggleItem = null;
|
|
}
|
|
}
|
|
|
|
/** Draws the little mouse pointer used as tray icon. */
|
|
private Image createTrayIconImage() {
|
|
BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
|
|
|
|
int[] xPoints = {3, 3, 6, 8, 10, 8, 12};
|
|
int[] yPoints = {1, 12, 8, 12, 11, 7, 7};
|
|
|
|
Graphics2D graphics = image.createGraphics();
|
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
graphics.setColor(Color.BLACK);
|
|
graphics.fillPolygon(xPoints, yPoints, xPoints.length);
|
|
graphics.setColor(Color.WHITE);
|
|
graphics.drawPolygon(xPoints, yPoints, xPoints.length);
|
|
graphics.dispose();
|
|
|
|
return image;
|
|
}
|
|
|
|
/** Stops the automation and releases everything the app is holding. */
|
|
private void cleanup() {
|
|
LOGGER.info("Shutting down...");
|
|
stopAutomation();
|
|
|
|
if (trayIcon != null) {
|
|
SystemTray.getSystemTray().remove(trayIcon);
|
|
trayIcon = null;
|
|
}
|
|
|
|
Main.unregisterJNativeHook();
|
|
}
|
|
|
|
/**
|
|
* Explains that the app cannot work without its permissions, and offers
|
|
* a shortcut to the right system settings pane. the hook is retried on
|
|
* demand, so the app can start working without a restart. must be called
|
|
* on the EDT, after the window is visible.
|
|
*/
|
|
public void showPermissionsHelp() {
|
|
Object[] options = {"Open System Settings", "Try Again", "Close"};
|
|
|
|
while (true) {
|
|
int choice = JOptionPane.showOptionDialog(frame,
|
|
"autosqueal needs the accessibility permission to move the mouse and to\n"
|
|
+ "listen for the ctrl+alt hotkey, and the screen recording one to\n"
|
|
+ "show the screen preview.\n\n"
|
|
+ "grant them in privacy & security, then press \"try again\".\n"
|
|
+ "if autosqueal already appears in the list, remove it with the\n"
|
|
+ "minus button and allow it again: every rebuild looks like a\n"
|
|
+ "different app to the system.",
|
|
"permissions needed",
|
|
JOptionPane.DEFAULT_OPTION,
|
|
JOptionPane.WARNING_MESSAGE,
|
|
null,
|
|
options,
|
|
options[0]);
|
|
|
|
if (choice == 0) {
|
|
openPrivacySettings();
|
|
} else if (choice == 1) {
|
|
if (Main.registerJNativeHook()) {
|
|
LOGGER.info("The native hook was registered on a later attempt!");
|
|
return;
|
|
}
|
|
} else {
|
|
// closed or gave up: leave the app running, the window and the
|
|
// tray icon still work
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Opens the privacy & security pane at the accessibility section. */
|
|
private void openPrivacySettings() {
|
|
if (SystemUtil.getHostSystem() != SystemUtil.OperatingSystem.MAC_OS) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
new ProcessBuilder("open",
|
|
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")
|
|
.start();
|
|
} catch (IOException ex) {
|
|
LOGGER.error("Could not open the system settings", ex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
SwingUtilities.invokeLater(() -> {
|
|
if (cursorMover == null) {
|
|
startAutomation();
|
|
} else {
|
|
stopAutomation();
|
|
}
|
|
updateToggleLabel();
|
|
});
|
|
}
|
|
|
|
private void startAutomation() {
|
|
LOGGER.info("Starting automation");
|
|
|
|
// periodic work (polling and movement cadence) on a classic scheduler,
|
|
// so that it can be scheduled with fixed delays
|
|
periodicScheduler = Executors.newScheduledThreadPool(2, runnable -> {
|
|
Thread thread = new Thread(runnable, "autosqueal-scheduler");
|
|
thread.setDaemon(true);
|
|
return thread;
|
|
});
|
|
|
|
// movements and clicks run on virtual threads: they mostly sleep,
|
|
// and blocking them costs nothing
|
|
movementExecutor = Executors.newThreadPerTaskExecutor(
|
|
Thread.ofVirtual().name("autosqueal-movement-").factory());
|
|
|
|
// away-detection: polls the cursor position and ignores the movements
|
|
// the app performs itself, as reported by the robot mouse tracker.
|
|
// the automation only moves the mouse when the user is away.
|
|
RobotMouseTracker robotTracker = new RobotMouseTracker();
|
|
CursorMoveListener cursorMoveListener = new CursorMoveListener(robotTracker);
|
|
periodicScheduler.scheduleWithFixedDelay(cursorMoveListener, 0L, AWAY_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
|
|
|
cursorMover = new CursorMover(movementExecutor, robotTracker, cursorMoveListener::isUserAway);
|
|
periodicScheduler.scheduleWithFixedDelay(cursorMover,
|
|
AUTOMATION_START_DELAY_SECONDS,
|
|
RunnerUtil.SECONDS_BETWEEN_MOVES,
|
|
TimeUnit.SECONDS);
|
|
}
|
|
|
|
private void stopAutomation() {
|
|
LOGGER.info("Stopping automation");
|
|
|
|
if (periodicScheduler != null) {
|
|
periodicScheduler.shutdownNow();
|
|
periodicScheduler = null;
|
|
}
|
|
|
|
if (movementExecutor != null) {
|
|
// interrupts the in-flight movement threads, too
|
|
movementExecutor.shutdownNow();
|
|
movementExecutor = 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) + "]";
|
|
|
|
String label = (cursorMover == null ? "Start " : "Stop ") + hotkey;
|
|
|
|
toggleButton.setText(label);
|
|
if (trayToggleItem != null) {
|
|
trayToggleItem.setLabel(label);
|
|
}
|
|
}
|
|
|
|
} |