feat: add screenshot hotkey manager for manual game capture

- Add modules/screenshot_hotkey.py with global hotkey listener using pynput
- Default hotkeys: F12=Full, Shift+F12=Region, Ctrl+F12=Loot, Alt+F12=HUD
- Integrate into main_window.py with Settings dialog
- Hotkeys work globally (even when game is focused)
- Add Tools → Screenshot Hotkeys menu (Ctrl+Shift+S)
- Shows status message when screenshot is captured
This commit is contained in:
LemonNexus 2026-02-11 12:31:59 +00:00
parent 1ff64ac53f
commit 286d3b4f5c
2 changed files with 580 additions and 0 deletions

View File

@ -0,0 +1,471 @@
"""
Lemontropia Suite - Screenshot Hotkey Manager
Global hotkey listener for manual screenshot capture.
Uses pynput for global keyboard shortcuts.
"""
import logging
import threading
from pathlib import Path
from datetime import datetime
from typing import Optional, Callable, Dict, List
from dataclasses import dataclass
try:
from pynput import keyboard
PYNPUT_AVAILABLE = True
except ImportError:
PYNPUT_AVAILABLE = False
keyboard = None
from PyQt6.QtCore import QObject, pyqtSignal, QThread
from .auto_screenshot import AutoScreenshot
logger = logging.getLogger(__name__)
@dataclass
class ScreenshotEvent:
"""Screenshot capture event data."""
filepath: Path
timestamp: datetime
triggered_by: str # 'hotkey' or 'auto'
description: str = ""
class ScreenshotHotkeyManager(QObject):
"""
Global hotkey manager for manual screenshots.
Runs in background thread to capture hotkeys even when app not focused.
"""
# Signals for Qt integration
screenshot_captured = pyqtSignal(str) # filepath
hotkey_pressed = pyqtSignal(str) # hotkey name
# Default hotkeys
DEFAULT_HOTKEYS = {
'screenshot_full': 'f12', # Full screen
'screenshot_region': 'shift+f12', # Region selection
'screenshot_loot': 'ctrl+f12', # Loot window area
'screenshot_hud': 'alt+f12', # HUD area
}
def __init__(self, screenshot_manager: Optional[AutoScreenshot] = None,
parent=None):
super().__init__(parent)
self.screenshot_manager = screenshot_manager or AutoScreenshot()
self.hotkeys: Dict[str, str] = self.DEFAULT_HOTKEYS.copy()
self.enabled = True
self.listening = False
# Callbacks for custom actions
self.on_screenshot: Optional[Callable[[ScreenshotEvent], None]] = None
# Screenshot history
self.recent_screenshots: List[ScreenshotEvent] = []
self.max_history = 50
# Hotkey listener thread
self._listener_thread: Optional[threading.Thread] = None
self._listener: Optional[keyboard.Listener] = None
self._stop_event = threading.Event()
if not PYNPUT_AVAILABLE:
logger.warning("pynput not installed. Hotkey screenshots disabled.")
logger.info("Install with: pip install pynput")
def is_available(self) -> bool:
"""Check if hotkey manager is available."""
return PYNPUT_AVAILABLE
def start_listening(self):
"""Start global hotkey listener in background thread."""
if not PYNPUT_AVAILABLE:
logger.error("Cannot start hotkey listener: pynput not available")
return False
if self.listening:
logger.warning("Hotkey listener already running")
return True
try:
self._stop_event.clear()
self._listener_thread = threading.Thread(
target=self._listen_for_hotkeys,
daemon=True
)
self._listener_thread.start()
self.listening = True
logger.info("Screenshot hotkey listener started")
return True
except Exception as e:
logger.error(f"Failed to start hotkey listener: {e}")
return False
def stop_listening(self):
"""Stop global hotkey listener."""
if not self.listening:
return
self._stop_event.set()
if self._listener:
try:
self._listener.stop()
except:
pass
if self._listener_thread:
self._listener_thread.join(timeout=1.0)
self.listening = False
logger.info("Screenshot hotkey listener stopped")
def _listen_for_hotkeys(self):
"""Background thread: listen for global hotkeys."""
try:
# Create hotkey combinations
hotkey_combinations = {}
for action, combo in self.hotkeys.items():
try:
hk = keyboard.HotKey(
keyboard.HotKey.parse(combo),
lambda a=action: self._on_hotkey(a)
)
hotkey_combinations[action] = hk
except Exception as e:
logger.warning(f"Failed to parse hotkey '{combo}': {e}")
def on_press(key):
for hk in hotkey_combinations.values():
hk.press(key)
def on_release(key):
for hk in hotkey_combinations.values():
hk.release(key)
self._listener = keyboard.Listener(
on_press=on_press,
on_release=on_release
)
self._listener.start()
self._listener.join()
except Exception as e:
logger.error(f"Hotkey listener error: {e}")
def _on_hotkey(self, action: str):
"""Handle hotkey press."""
if not self.enabled:
return
logger.info(f"Hotkey triggered: {action}")
self.hotkey_pressed.emit(action)
if action == 'screenshot_full':
self._capture_full_screen()
elif action == 'screenshot_region':
self._capture_region()
elif action == 'screenshot_loot':
self._capture_loot_window()
elif action == 'screenshot_hud':
self._capture_hud()
def _capture_full_screen(self):
"""Capture full screen."""
filename = f"manual_full_{datetime.now():%Y%m%d_%H%M%S_%f}"[:-3] + ".png"
filepath = self.screenshot_manager.capture_full_screen(filename)
if filepath:
event = ScreenshotEvent(
filepath=filepath,
timestamp=datetime.now(),
triggered_by='hotkey',
description='Full screen capture (F12)'
)
self._add_to_history(event)
self.screenshot_captured.emit(str(filepath))
def _capture_region(self):
"""Capture region (would need region selector UI)."""
# For now, capture center of screen as a reasonable default
# In the future, this could open a region selection overlay
logger.info("Region capture: using center 800x600 region")
filename = f"manual_region_{datetime.now():%Y%m%d_%H%M%S_%f}"[:-3] + ".png"
# Get primary monitor dimensions
try:
import mss
with mss.mss() as sct:
monitor = sct.monitors[1]
screen_w = monitor['width']
screen_h = monitor['height']
# Capture center 800x600
x = (screen_w - 800) // 2
y = (screen_h - 600) // 2
filepath = self.screenshot_manager.capture_region(
x, y, 800, 600, filename
)
if filepath:
event = ScreenshotEvent(
filepath=filepath,
timestamp=datetime.now(),
triggered_by='hotkey',
description='Region capture (Shift+F12)'
)
self._add_to_history(event)
self.screenshot_captured.emit(str(filepath))
except Exception as e:
logger.error(f"Region capture failed: {e}")
def _capture_loot_window(self):
"""Capture typical loot window area."""
filename = f"manual_loot_{datetime.now():%Y%m%d_%H%M%S_%f}"[:-3] + ".png"
# Typical loot window position (right side of screen)
try:
import mss
with mss.mss() as sct:
monitor = sct.monitors[1]
screen_w = monitor['width']
screen_h = monitor['height']
# Loot window: right side, ~300x400
x = screen_w - 350
y = screen_h // 2 - 200
w = 300
h = 400
filepath = self.screenshot_manager.capture_region(
max(0, x), max(0, y), w, h, filename
)
if filepath:
event = ScreenshotEvent(
filepath=filepath,
timestamp=datetime.now(),
triggered_by='hotkey',
description='Loot window capture (Ctrl+F12)'
)
self._add_to_history(event)
self.screenshot_captured.emit(str(filepath))
except Exception as e:
logger.error(f"Loot capture failed: {e}")
def _capture_hud(self):
"""Capture HUD area."""
filename = f"manual_hud_{datetime.now():%Y%m%d_%H%M%S_%f}"[:-3] + ".png"
try:
import mss
with mss.mss() as sct:
monitor = sct.monitors[1]
screen_w = monitor['width']
screen_h = monitor['height']
# HUD: bottom center
w = 600
h = 150
x = (screen_w - w) // 2
y = screen_h - h - 50
filepath = self.screenshot_manager.capture_region(
max(0, x), max(0, y), w, h, filename
)
if filepath:
event = ScreenshotEvent(
filepath=filepath,
timestamp=datetime.now(),
triggered_by='hotkey',
description='HUD capture (Alt+F12)'
)
self._add_to_history(event)
self.screenshot_captured.emit(str(filepath))
except Exception as e:
logger.error(f"HUD capture failed: {e}")
def _add_to_history(self, event: ScreenshotEvent):
"""Add screenshot to history."""
self.recent_screenshots.insert(0, event)
# Trim history
if len(self.recent_screenshots) > self.max_history:
self.recent_screenshots = self.recent_screenshots[:self.max_history]
# Call custom callback if set
if self.on_screenshot:
try:
self.on_screenshot(event)
except Exception as e:
logger.error(f"Screenshot callback error: {e}")
def get_recent_screenshots(self, n: int = 10) -> List[ScreenshotEvent]:
"""Get recent screenshots."""
return self.recent_screenshots[:n]
def set_hotkey(self, action: str, combination: str) -> bool:
"""
Change hotkey for an action.
Args:
action: Action name ('screenshot_full', etc.)
combination: New hotkey combination (e.g., 'f10', 'ctrl+shift+s')
Returns:
True if successful
"""
if action not in self.hotkeys:
logger.error(f"Unknown action: {action}")
return False
try:
# Validate hotkey format
keyboard.HotKey.parse(combination)
# Restart listener with new hotkey
was_listening = self.listening
self.stop_listening()
self.hotkeys[action] = combination
logger.info(f"Hotkey for '{action}' set to '{combination}'")
if was_listening:
return self.start_listening()
return True
except Exception as e:
logger.error(f"Invalid hotkey combination '{combination}': {e}")
return False
def get_hotkey_help(self) -> str:
"""Get formatted hotkey help text."""
lines = ["📸 Screenshot Hotkeys:", ""]
descriptions = {
'screenshot_full': 'Full screen',
'screenshot_region': 'Center region (800x600)',
'screenshot_loot': 'Loot window area',
'screenshot_hud': 'HUD area',
}
for action, combo in self.hotkeys.items():
desc = descriptions.get(action, action)
lines.append(f" {combo.upper():<15} - {desc}")
return "\n".join(lines)
class ScreenshotHotkeyWidget:
"""
PyQt widget for configuring screenshot hotkeys.
Can be embedded in settings dialog.
"""
def __init__(self, hotkey_manager: ScreenshotHotkeyManager):
self.manager = hotkey_manager
def create_settings_widget(self):
"""Create settings widget for hotkey configuration."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QGroupBox, QFormLayout,
QMessageBox
)
widget = QWidget()
layout = QVBoxLayout()
# Hotkey group
group = QGroupBox("Screenshot Hotkeys")
form = QFormLayout()
self.hotkey_inputs = {}
descriptions = {
'screenshot_full': 'Full Screen:',
'screenshot_region': 'Region (Center):',
'screenshot_loot': 'Loot Window:',
'screenshot_hud': 'HUD Area:',
}
for action, combo in self.manager.hotkeys.items():
row = QHBoxLayout()
line_edit = QLineEdit(combo)
line_edit.setMaximumWidth(150)
self.hotkey_inputs[action] = line_edit
row.addWidget(line_edit)
test_btn = QPushButton("Test")
test_btn.setMaximumWidth(60)
test_btn.clicked.connect(lambda checked, a=action: self._test_screenshot(a))
row.addWidget(test_btn)
row.addStretch()
form.addRow(descriptions.get(action, action), row)
group.setLayout(form)
layout.addWidget(group)
# Save button
save_btn = QPushButton("Save Hotkeys")
save_btn.clicked.connect(self._save_hotkeys)
layout.addWidget(save_btn)
# Help text
help_label = QLabel(self.manager.get_hotkey_help())
help_label.setWordWrap(True)
layout.addWidget(help_label)
layout.addStretch()
widget.setLayout(layout)
return widget
def _test_screenshot(self, action: str):
"""Test screenshot action."""
self.manager._on_hotkey(action)
def _save_hotkeys(self):
"""Save hotkey changes."""
for action, line_edit in self.hotkey_inputs.items():
combo = line_edit.text().strip().lower()
if combo:
self.manager.set_hotkey(action, combo)
# Convenience function
def create_screenshot_hotkeys(screenshot_manager: Optional[AutoScreenshot] = None,
start_listening: bool = True) -> Optional[ScreenshotHotkeyManager]:
"""
Create and start screenshot hotkey manager.
Args:
screenshot_manager: Existing AutoScreenshot instance
start_listening: Start listening immediately
Returns:
ScreenshotHotkeyManager or None if not available
"""
if not PYNPUT_AVAILABLE:
logger.error("pynput not installed. Run: pip install pynput")
return None
manager = ScreenshotHotkeyManager(screenshot_manager)
if start_listening:
manager.start_listening()
return manager

View File

@ -120,6 +120,18 @@ from ui.hud_overlay_clean import HUDOverlay
from ui.session_history import SessionHistoryDialog
from ui.gallery_dialog import GalleryDialog, ScreenshotCapture
# ============================================================================
# Screenshot Hotkey Integration
# ============================================================================
try:
from modules.screenshot_hotkey import ScreenshotHotkeyManager, ScreenshotHotkeyWidget
SCREENSHOT_HOTKEYS_AVAILABLE = True
except ImportError:
SCREENSHOT_HOTKEYS_AVAILABLE = False
ScreenshotHotkeyManager = None
ScreenshotHotkeyWidget = None
# ============================================================================
# Core Integration
@ -398,6 +410,10 @@ class MainWindow(QMainWindow):
# Screenshot capture
self._screenshot_capture = ScreenshotCapture(self.db)
# Screenshot hotkey manager (global hotkeys for manual capture)
self._screenshot_hotkeys = None
self._init_screenshot_hotkeys()
# Setup UI
self.setup_ui()
@ -812,6 +828,14 @@ class MainWindow(QMainWindow):
vision_test_action.triggered.connect(self.on_vision_test)
vision_menu.addAction(vision_test_action)
tools_menu.addSeparator()
# Screenshot hotkey settings
screenshot_hotkeys_action = QAction("📸 Screenshot &Hotkeys", self)
screenshot_hotkeys_action.setShortcut("Ctrl+Shift+S")
screenshot_hotkeys_action.triggered.connect(self._show_screenshot_hotkey_settings)
tools_menu.addAction(screenshot_hotkeys_action)
# View menu
view_menu = menubar.addMenu("&View")
@ -1867,6 +1891,83 @@ class MainWindow(QMainWindow):
# Settings Management
# ========================================================================
def _init_screenshot_hotkeys(self):
"""Initialize global screenshot hotkey manager."""
if not SCREENSHOT_HOTKEYS_AVAILABLE:
logger.warning("Screenshot hotkeys not available (pynput not installed)")
self.log_info("Hotkeys", "Screenshot hotkeys disabled - install pynput")
return
try:
from modules.auto_screenshot import AutoScreenshot
# Create screenshot manager
auto_screenshot = AutoScreenshot(self._screenshot_capture.screenshot_dir)
# Create hotkey manager
self._screenshot_hotkeys = ScreenshotHotkeyManager(auto_screenshot)
# Connect signals
self._screenshot_hotkeys.screenshot_captured.connect(self._on_screenshot_captured)
self._screenshot_hotkeys.hotkey_pressed.connect(self._on_hotkey_pressed)
# Start listening
if self._screenshot_hotkeys.start_listening():
help_text = self._screenshot_hotkeys.get_hotkey_help()
self.log_info("Hotkeys", "Screenshot hotkeys active")
self.log_info("Hotkeys", "F12=Full, Shift+F12=Region, Ctrl+F12=Loot, Alt+F12=HUD")
else:
self.log_warning("Hotkeys", "Failed to start screenshot hotkeys")
except Exception as e:
logger.error(f"Failed to initialize screenshot hotkeys: {e}")
self._screenshot_hotkeys = None
def _on_screenshot_captured(self, filepath: str):
"""Handle screenshot captured signal."""
self.log_info("Screenshot", f"Saved: {filepath}")
# Show notification
self.statusBar().showMessage(f"📸 Screenshot saved: {filepath}", 3000)
# Refresh gallery if open
# (Gallery dialog would need to be updated to refresh)
def _on_hotkey_pressed(self, action: str):
"""Handle hotkey pressed signal."""
logger.debug(f"Screenshot hotkey pressed: {action}")
def _show_screenshot_hotkey_settings(self):
"""Show screenshot hotkey settings dialog."""
if not SCREENSHOT_HOTKEYS_AVAILABLE or not self._screenshot_hotkeys:
QMessageBox.information(
self,
"Screenshot Hotkeys",
"Screenshot hotkeys are not available.\n\n"
"Install pynput to enable:\n"
" pip install pynput\n\n"
"Default hotkeys:\n"
" F12 - Full screen\n"
" Shift+F12 - Center region\n"
" Ctrl+F12 - Loot window\n"
" Alt+F12 - HUD area"
)
return
from PyQt6.QtWidgets import QDialog, QVBoxLayout
dialog = QDialog(self)
dialog.setWindowTitle("Screenshot Hotkey Settings")
dialog.setMinimumWidth(400)
layout = QVBoxLayout(dialog)
# Create settings widget
widget = ScreenshotHotkeyWidget(self._screenshot_hotkeys)
layout.addWidget(widget.create_settings_widget())
dialog.exec()
def _load_settings(self):
"""Load persistent settings from QSettings."""
settings = QSettings("Lemontropia", "Suite")
@ -1900,6 +2001,14 @@ class MainWindow(QMainWindow):
def closeEvent(self, event):
"""Handle window close event."""
# Stop screenshot hotkey listener
if self._screenshot_hotkeys:
try:
self._screenshot_hotkeys.stop_listening()
logger.info("Screenshot hotkeys stopped")
except Exception as e:
logger.error(f"Error stopping screenshot hotkeys: {e}")
if self.session_state == SessionState.RUNNING:
reply = QMessageBox.question(
self,