473 lines
16 KiB
Python
473 lines
16 KiB
Python
"""
|
|
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)
|
|
# Remove angle brackets for display
|
|
display_combo = combo.replace('<', '').replace('>', '')
|
|
lines.append(f" {display_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 |