fixed terminate

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Radek Davidek 2026-04-30 18:23:16 +02:00
parent 0998ba3762
commit b3cf1825d9
3 changed files with 157 additions and 69 deletions

View File

@ -7,40 +7,43 @@ import java.util.Set;
import javax.swing.JLabel; import javax.swing.JLabel;
/** /**
* Globální hotkey listener pro detekci CTRL+ALT+Z kombinace * Globální hotkey listener pro detekci CTRL+ALT+Q.
* Funguje i když není okno fokusované * Na Linuxu funguje i bez fokusu přes nativní polling X11.
*/ */
public class GlobalHotkey extends JLabel { public class GlobalHotkey extends JLabel {
private static GlobalHotkey instance; private static GlobalHotkey instance;
private GlobalKeyListener globalKeyListener; private static final long HOTKEY_COOLDOWN_MS = 700;
private final GlobalKeyListener globalKeyListener;
private final Runnable onHotkey;
private volatile boolean running = true;
private volatile long lastHotkeyTrigger = 0L;
private Thread pollingThread;
public GlobalHotkey(Runnable onHotkey) { public GlobalHotkey(Runnable onHotkey) {
this.globalKeyListener = new GlobalKeyListener(onHotkey); this.onHotkey = onHotkey;
this.globalKeyListener = new GlobalKeyListener();
// Registrace globálního key listeneru pro případy, kdy aplikace fokus // Registrace globálního key listeneru pro případy, kdy aplikace fokus
KeyboardFocusManager.getCurrentKeyboardFocusManager() KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addKeyEventDispatcher(globalKeyListener); .addKeyEventDispatcher(globalKeyListener);
// Spustit nativní polling pro globální STOP hotkey na Linuxu (funguje i bez fokusu) // Nativní polling globální hotkey na Linuxu (funguje i bez fokusu)
if (System.getProperty("os.name").toLowerCase().contains("linux")) { if (isLinux()) {
Thread pollingThread = new Thread(() -> { pollingThread = new Thread(() -> {
while (true) { while (running && !Thread.currentThread().isInterrupted()) {
try { try {
Thread.sleep(150); Thread.sleep(150);
// Kontrola CTRL+ALT+X globálně // XK_Control_L/R: 0xFFE3/4, XK_Alt_L/R: 0xFFE9/A, XK_q: 0x71, XK_Q: 0x51
// XK_Control_L/R: 0xFFE3/4, XK_Alt_L/R: 0xFFE9/A, XK_x: 0x78, XK_X: 0x58
boolean ctrl = WindowFinder.isKeyPressedGlobally(0xFFE3) || WindowFinder.isKeyPressedGlobally(0xFFE4); boolean ctrl = WindowFinder.isKeyPressedGlobally(0xFFE3) || WindowFinder.isKeyPressedGlobally(0xFFE4);
boolean alt = WindowFinder.isKeyPressedGlobally(0xFFE9) || WindowFinder.isKeyPressedGlobally(0xFFEA); boolean alt = WindowFinder.isKeyPressedGlobally(0xFFE9) || WindowFinder.isKeyPressedGlobally(0xFFEA);
boolean xPressed = WindowFinder.isKeyPressedGlobally(0x78) || WindowFinder.isKeyPressedGlobally(0x58); boolean qPressed = WindowFinder.isKeyPressedGlobally(0x71) || WindowFinder.isKeyPressedGlobally(0x51);
if (ctrl && alt && xPressed) { if (ctrl && alt && qPressed) {
if (onHotkey != null) { triggerHotkey("Globální NATIVNÍ hotkey detekován: CTRL+ALT+Q");
System.out.println("🔌 Globální NATIVNÍ hotkey detekován: CTRL+ALT+X");
onHotkey.run();
Thread.sleep(1000); // Prevence vícenásobného spuštění
}
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt();
break; break;
} catch (Exception e) { } catch (Exception e) {
// Ignorovat chyby v pollingu // Ignorovat chyby v pollingu
@ -62,7 +65,7 @@ public class GlobalHotkey extends JLabel {
public boolean isKeyPressed(int keyCode) { public boolean isKeyPressed(int keyCode) {
// Na Linuxu pro šipky používáme výhradně nativní globální detekci. // Na Linuxu pro šipky používáme výhradně nativní globální detekci.
// Standardní KeyEventDispatcher se může "zaseknout", pokud okno ztratí fokus během stisku. // Standardní KeyEventDispatcher se může "zaseknout", pokud okno ztratí fokus během stisku.
if (System.getProperty("os.name").toLowerCase().contains("linux")) { if (isLinux()) {
if (keyCode == KeyEvent.VK_DOWN) { if (keyCode == KeyEvent.VK_DOWN) {
try { try {
return WindowFinder.isKeyPressedGlobally(0xFF54); // XK_Down return WindowFinder.isKeyPressedGlobally(0xFF54); // XK_Down
@ -76,17 +79,40 @@ public class GlobalHotkey extends JLabel {
return globalKeyListener.isKeyPressed(keyCode); return globalKeyListener.isKeyPressed(keyCode);
} }
public void cleanup() {
running = false;
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.removeKeyEventDispatcher(globalKeyListener);
if (pollingThread != null) {
pollingThread.interrupt();
pollingThread = null;
}
}
private static boolean isLinux() {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
private void triggerHotkey(String logMessage) {
long now = System.currentTimeMillis();
if (now - lastHotkeyTrigger < HOTKEY_COOLDOWN_MS) {
return;
}
lastHotkeyTrigger = now;
if (onHotkey != null) {
System.out.println("🔌 " + logMessage);
onHotkey.run();
}
}
/** /**
* Interní třída pro globální naslouchání na klávesnici * Interní třída pro globální naslouchání na klávesnici
*/ */
private static class GlobalKeyListener implements java.awt.KeyEventDispatcher { private class GlobalKeyListener implements java.awt.KeyEventDispatcher {
private Runnable callback;
private Set<Integer> pressedKeys = new HashSet<>(); private Set<Integer> pressedKeys = new HashSet<>();
public GlobalKeyListener(Runnable callback) {
this.callback = callback;
}
public boolean isKeyPressed(int keyCode) { public boolean isKeyPressed(int keyCode) {
return pressedKeys.contains(keyCode); return pressedKeys.contains(keyCode);
} }
@ -96,13 +122,10 @@ public class GlobalHotkey extends JLabel {
if (e.getID() == KeyEvent.KEY_PRESSED) { if (e.getID() == KeyEvent.KEY_PRESSED) {
pressedKeys.add(e.getKeyCode()); pressedKeys.add(e.getKeyCode());
// Detekce CTRL+ALT+X // Detekce CTRL+ALT+Q
if (e.getKeyCode() == KeyEvent.VK_X && if (e.getKeyCode() == KeyEvent.VK_Q &&
e.isControlDown() && e.isAltDown()) { e.isControlDown() && e.isAltDown()) {
System.out.println("🔌 Globální hotkey detekován: CTRL+ALT+X"); triggerHotkey("Globální hotkey detekován: CTRL+ALT+Q");
if (callback != null) {
callback.run();
}
return true; // Konzumovat event return true; // Konzumovat event
} }
} else if (e.getID() == KeyEvent.KEY_RELEASED) { } else if (e.getID() == KeyEvent.KEY_RELEASED) {

View File

@ -1,15 +1,11 @@
package com.pokemongo; package com.pokemongo;
import java.awt.AWTException; import java.awt.AWTException;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.MouseInfo; import java.awt.MouseInfo;
import java.awt.Point; import java.awt.Point;
import java.awt.PointerInfo; import java.awt.PointerInfo;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.awt.Robot; import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.InputEvent; import java.awt.event.InputEvent;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
@ -35,6 +31,7 @@ public class PokemonGoAutomation {
private static final int DELAY_AFTER_ACTION = 100; // ms private static final int DELAY_AFTER_ACTION = 100; // ms
private int transferredPokemonCount = 0; // Počet transfernutých pokémonů private int transferredPokemonCount = 0; // Počet transfernutých pokémonů
private Point initialMousePosition = null; // Pozice kurzoru před začátkem automatizace private Point initialMousePosition = null; // Pozice kurzoru před začátkem automatizace
private volatile boolean stopRequested = false;
public PokemonGoAutomation() throws AWTException { public PokemonGoAutomation() throws AWTException {
this.robot = new Robot(); this.robot = new Robot();
@ -57,6 +54,14 @@ public class PokemonGoAutomation {
transferredPokemonCount = 0; transferredPokemonCount = 0;
} }
public void requestStop() {
stopRequested = true;
}
private boolean shouldStop() {
return stopRequested || Thread.currentThread().isInterrupted();
}
/** /**
* Pořídí screenshot oblasti okna * Pořídí screenshot oblasti okna
*/ */
@ -120,6 +125,10 @@ public class PokemonGoAutomation {
if (template == null) if (template == null)
return 0.0; return 0.0;
if (shouldStop()) {
return 0.0;
}
int tWidth = template.getWidth(); int tWidth = template.getWidth();
int tHeight = template.getHeight(); int tHeight = template.getHeight();
@ -136,6 +145,9 @@ public class PokemonGoAutomation {
int requiredMatch = (int) (totalPixels * 0.75); // 75% shody int requiredMatch = (int) (totalPixels * 0.75); // 75% shody
for (int y = 0; y < tHeight; y++) { for (int y = 0; y < tHeight; y++) {
if (shouldStop()) {
return 0.0;
}
for (int x = 0; x < tWidth; x++) { for (int x = 0; x < tWidth; x++) {
int templateRGB = template.getRGB(x, y); int templateRGB = template.getRGB(x, y);
int screenRGB = screenshot.getRGB(startX + x, startY + y); int screenRGB = screenshot.getRGB(startX + x, startY + y);
@ -175,6 +187,10 @@ public class PokemonGoAutomation {
private Point findConfirmTransferButtonPosition(BufferedImage template, int tolerance) { private Point findConfirmTransferButtonPosition(BufferedImage template, int tolerance) {
System.out.println("Hledám potvrzovací TRANSFER tlačítko pomocí template matchingu..."); System.out.println("Hledám potvrzovací TRANSFER tlačítko pomocí template matchingu...");
if (shouldStop()) {
return null;
}
BufferedImage screenshot = captureScreen(windowBounds); BufferedImage screenshot = captureScreen(windowBounds);
int tWidth = template.getWidth(); int tWidth = template.getWidth();
@ -190,6 +206,9 @@ public class PokemonGoAutomation {
// Skenovat Y-ové pozice s větším krokem - 15px místo 5px pro zrychlení // Skenovat Y-ové pozice s větším krokem - 15px místo 5px pro zrychlení
for (int y = searchStartY; y <= searchEndY - tHeight; y += 15) { for (int y = searchStartY; y <= searchEndY - tHeight; y += 15) {
if (shouldStop()) {
return null;
}
// Skenovat také X pozice pro nalezení středu shody // Skenovat také X pozice pro nalezení středu shody
for (int x = 0; x <= windowBounds.width - tWidth; x += 30) { for (int x = 0; x <= windowBounds.width - tWidth; x += 30) {
double score = templateMatch(screenshot, x, y, template, tolerance); double score = templateMatch(screenshot, x, y, template, tolerance);
@ -218,6 +237,10 @@ public class PokemonGoAutomation {
private Point findIncludeButtonPosition(int tolerance) { private Point findIncludeButtonPosition(int tolerance) {
System.out.println("Hledám tlačítko INCLUDE pomocí template matchingu (include.png)..."); System.out.println("Hledám tlačítko INCLUDE pomocí template matchingu (include.png)...");
if (shouldStop()) {
return null;
}
BufferedImage screenshot = captureScreen(windowBounds); BufferedImage screenshot = captureScreen(windowBounds);
int tWidth = includeTemplate.getWidth(); int tWidth = includeTemplate.getWidth();
@ -233,6 +256,9 @@ public class PokemonGoAutomation {
// Skenovat Y-ové pozice s větším krokem - 15px místo 5px pro zrychlení // Skenovat Y-ové pozice s větším krokem - 15px místo 5px pro zrychlení
for (int y = searchStartY; y <= searchEndY - tHeight; y += 15) { for (int y = searchStartY; y <= searchEndY - tHeight; y += 15) {
if (shouldStop()) {
return null;
}
// Skenovat také X pozice pro nalezení středu shody // Skenovat také X pozice pro nalezení středu shody
for (int x = 0; x <= windowBounds.width - tWidth; x += 30) { for (int x = 0; x <= windowBounds.width - tWidth; x += 30) {
double score = templateMatch(screenshot, x, y, includeTemplate, tolerance); double score = templateMatch(screenshot, x, y, includeTemplate, tolerance);
@ -309,6 +335,10 @@ public class PokemonGoAutomation {
* Stiskne tlačítko Transfer na spodní části obrazovky * Stiskne tlačítko Transfer na spodní části obrazovky
*/ */
public void clickTransferButton() { public void clickTransferButton() {
if (shouldStop()) {
return;
}
// Použít detekci tlačítka // Použít detekci tlačítka
// Point buttonPos = findTransferButtonPosition(); // Point buttonPos = findTransferButtonPosition();
Point buttonPos = getAbsolutePoint(300, 1156); Point buttonPos = getAbsolutePoint(300, 1156);
@ -334,6 +364,10 @@ public class PokemonGoAutomation {
public void clickConfirmTransferButton(BufferedImage template, int tolerance) { public void clickConfirmTransferButton(BufferedImage template, int tolerance) {
System.out.println("Potvrzuji transfer - hledám confirmation button..."); System.out.println("Potvrzuji transfer - hledám confirmation button...");
if (shouldStop()) {
return;
}
robot.delay(100); // Počkat na zobrazení dialogu robot.delay(100); // Počkat na zobrazení dialogu
// Najít zelené TRANSFER tlačítko v potvrzovacím dialogu // Najít zelené TRANSFER tlačítko v potvrzovacím dialogu
@ -356,6 +390,10 @@ public class PokemonGoAutomation {
public boolean clickIncludeButton(int tolerance) { public boolean clickIncludeButton(int tolerance) {
System.out.println("Potvrzuji transfer - hledám include.png (confirmation button)..."); System.out.println("Potvrzuji transfer - hledám include.png (confirmation button)...");
if (shouldStop()) {
return false;
}
robot.delay(100); // Počkat na zobrazení dialogu robot.delay(100); // Počkat na zobrazení dialogu
// Najít zelené INCLUDE tlačítko v potvrzovacím dialogu // Najít zelené INCLUDE tlačítko v potvrzovacím dialogu
@ -409,7 +447,7 @@ public class PokemonGoAutomation {
try { try {
while (transferredCount < totalPokemonCount) { while (transferredCount < totalPokemonCount) {
// Kontrola přerušení // Kontrola přerušení
if (Thread.currentThread().isInterrupted()) { if (shouldStop()) {
System.out.println("\n⚠ Automatizace byla přerušena!"); System.out.println("\n⚠ Automatizace byla přerušena!");
return; return;
} }
@ -423,9 +461,19 @@ public class PokemonGoAutomation {
// Vybrat pokémony a získat skutečný počet vybraných // Vybrat pokémony a získat skutečný počet vybraných
int actualTransferredCount = selectAllPokemonCount(pokemonThisRound); int actualTransferredCount = selectAllPokemonCount(pokemonThisRound);
if (shouldStop()) {
System.out.println("\n⚠ Automatizace byla přerušena během označování!");
return;
}
System.out.println("Čekám na TRANSFER..."); System.out.println("Čekám na TRANSFER...");
clickTransferButton(); clickTransferButton();
if (shouldStop()) {
System.out.println("\n⚠ Automatizace byla přerušena po kliknutí na TRANSFER!");
return;
}
System.out.println("Čekám na INCLUDE dialog..."); System.out.println("Čekám na INCLUDE dialog...");
robot.delay(100); robot.delay(100);
if (clickIncludeButton(110)) { if (clickIncludeButton(110)) {
@ -450,7 +498,7 @@ public class PokemonGoAutomation {
// Čekat s kontrolou přerušení // Čekat s kontrolou přerušení
long endTime = System.currentTimeMillis() + (delaySeconds * 1000L); long endTime = System.currentTimeMillis() + (delaySeconds * 1000L);
while (System.currentTimeMillis() < endTime) { while (System.currentTimeMillis() < endTime) {
if (Thread.currentThread().isInterrupted()) { if (shouldStop()) {
System.out.println("\n⚠ Automatizace byla přerušena během přestávky!"); System.out.println("\n⚠ Automatizace byla přerušena během přestávky!");
break; break;
} }
@ -487,6 +535,11 @@ public class PokemonGoAutomation {
System.out.println("Budu označovat " + maxPokemon + " pokémonů..."); System.out.println("Budu označovat " + maxPokemon + " pokémonů...");
for (int i = 0; i < maxPokemon; i++) { for (int i = 0; i < maxPokemon; i++) {
if (shouldStop()) {
System.out.println("Označování přerušeno.");
return i;
}
Point pos = positions.get(i); Point pos = positions.get(i);
System.out.println("Klikám na Pokémona " + (i + 1) + "/" + maxPokemon + " na pozici: " + pos); System.out.println("Klikám na Pokémona " + (i + 1) + "/" + maxPokemon + " na pozici: " + pos);

View File

@ -96,7 +96,7 @@ public class PokemonGoGUI extends JFrame {
versionLabel.setFont(SMALL_FONT); versionLabel.setFont(SMALL_FONT);
versionLabel.setForeground(new Color(120, 120, 120)); versionLabel.setForeground(new Color(120, 120, 120));
stopButton = new JButton("⏹ ZASTAVIT (CTRL+ALT+X)"); stopButton = new JButton("⏹ ZASTAVIT (CTRL+ALT+Q)");
stopButton.setPreferredSize(new Dimension(150, 30)); stopButton.setPreferredSize(new Dimension(150, 30));
styleButton(stopButton, PRIMARY_RED, Color.WHITE, SMALL_FONT); styleButton(stopButton, PRIMARY_RED, Color.WHITE, SMALL_FONT);
stopButton.setEnabled(false); stopButton.setEnabled(false);
@ -261,8 +261,8 @@ public class PokemonGoGUI extends JFrame {
add(mainPanel); add(mainPanel);
// Klávesová zkratka: Ctrl+Alt+Z pro zastavení // Klávesová zkratka: Ctrl+Alt+Q pro zastavení
KeyStroke hotkey = KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); KeyStroke hotkey = KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK);
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(hotkey, "stopAutomationAction"); getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(hotkey, "stopAutomationAction");
getRootPane().getActionMap().put("stopAutomationAction", new AbstractAction() { getRootPane().getActionMap().put("stopAutomationAction", new AbstractAction() {
@Override @Override
@ -273,7 +273,7 @@ public class PokemonGoGUI extends JFrame {
} }
}); });
// Globální hotkey pro ESC (bonus, když okno není fokusované) // Globální hotkey (funguje i když okno není fokusované)
globalHotkey = new GlobalHotkey(new Runnable() { globalHotkey = new GlobalHotkey(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -290,7 +290,7 @@ public class PokemonGoGUI extends JFrame {
logArea.append("=== Pokémon GO Automatizace ===\n\n"); logArea.append("=== Pokémon GO Automatizace ===\n\n");
logArea.append("Vyberte jednu z automatizací a klikněte na tlačítko START.\n"); logArea.append("Vyberte jednu z automatizací a klikněte na tlačítko START.\n");
logArea.append("Ujistěte se, že je aplikace Pokémon GO spuštěná.\n"); logArea.append("Ujistěte se, že je aplikace Pokémon GO spuštěná.\n");
logArea.append("Pro zastavení stiskněte Ctrl+Alt+X nebo klikněte ZASTAVIT.\n\n"); logArea.append("Pro zastavení stiskněte Ctrl+Alt+Q nebo klikněte ZASTAVIT.\n\n");
} }
/** /**
@ -482,6 +482,7 @@ public class PokemonGoGUI extends JFrame {
} }
// Získat počet pokémonů a čekání z GUI // Získat počet pokémonů a čekání z GUI
@SuppressWarnings("unchecked")
JComboBox<Integer> countComboBox = (JComboBox<Integer>) transferCard.getClientProperty("countComboBox"); JComboBox<Integer> countComboBox = (JComboBox<Integer>) transferCard.getClientProperty("countComboBox");
JSpinner delaySpinner = (JSpinner) transferCard.getClientProperty("delaySpinner"); JSpinner delaySpinner = (JSpinner) transferCard.getClientProperty("delaySpinner");
@ -742,10 +743,17 @@ public class PokemonGoGUI extends JFrame {
* Zastaví automatizaci * Zastaví automatizaci
*/ */
private void stopAutomation() { private void stopAutomation() {
if (!isRunning && !autoClickRunning) {
return;
}
shouldStop = true; shouldStop = true;
autoClickRunning = false; autoClickRunning = false;
isRunning = false; isRunning = false;
stopButton.setEnabled(false);
if (automation != null) {
automation.requestStop();
}
if (automationThread != null && automationThread.isAlive()) { if (automationThread != null && automationThread.isAlive()) {
automationThread.interrupt(); automationThread.interrupt();
@ -760,6 +768,7 @@ public class PokemonGoGUI extends JFrame {
SwingUtilities.invokeLater(new Runnable() { SwingUtilities.invokeLater(new Runnable() {
@Override @Override
public void run() { public void run() {
stopButton.setEnabled(false);
logArea.append("\n⚠ Automatizace byla přerušena uživatelem\n"); logArea.append("\n⚠ Automatizace byla přerušena uživatelem\n");
statusLabel.setText("Přerušeno"); statusLabel.setText("Přerušeno");
statusLabel.setForeground(new Color(244, 67, 54)); statusLabel.setForeground(new Color(244, 67, 54));
@ -852,6 +861,9 @@ public class PokemonGoGUI extends JFrame {
@Override @Override
public void windowClosing(WindowEvent e) { public void windowClosing(WindowEvent e) {
System.out.println("Zavírám okno..."); System.out.println("Zavírám okno...");
if (frame.globalHotkey != null) {
frame.globalHotkey.cleanup();
}
frame.saveSettings(); frame.saveSettings();
System.exit(0); System.exit(0);
} }