Lemontropia-Suite/modules/screenshot_hotkey.py

471 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)
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