485 lines
17 KiB
Python
485 lines
17 KiB
Python
"""
|
||
Lemontropia Suite - Screenshot Hotkey Manager (Windows-friendly version)
|
||
Uses 'keyboard' library for reliable global hotkeys on Windows.
|
||
Falls back to Qt shortcuts if global hotkeys fail.
|
||
"""
|
||
|
||
import logging
|
||
import threading
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
from typing import Optional, Callable, Dict, List
|
||
from dataclasses import dataclass
|
||
|
||
from PyQt6.QtCore import QObject, pyqtSignal
|
||
from PyQt6.QtGui import QKeySequence, QShortcut
|
||
from PyQt6.QtWidgets import QMainWindow
|
||
|
||
try:
|
||
import keyboard
|
||
KEYBOARD_LIB_AVAILABLE = True
|
||
except ImportError:
|
||
KEYBOARD_LIB_AVAILABLE = False
|
||
keyboard = None
|
||
|
||
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):
|
||
"""
|
||
Screenshot hotkey manager with Qt shortcuts + optional global hotkeys.
|
||
|
||
Uses Qt QShortcut (works when app is focused) as primary method.
|
||
Falls back to global hotkeys via 'keyboard' library if available.
|
||
"""
|
||
|
||
# Signals for Qt integration
|
||
screenshot_captured = pyqtSignal(str) # filepath
|
||
hotkey_pressed = pyqtSignal(str) # hotkey name
|
||
|
||
# Default hotkeys (Qt format)
|
||
DEFAULT_HOTKEYS = {
|
||
'screenshot_full': 'F12',
|
||
'screenshot_region': 'Shift+F12',
|
||
'screenshot_loot': 'Ctrl+F12',
|
||
'screenshot_hud': 'Alt+F12',
|
||
}
|
||
|
||
def __init__(self, parent_window: QMainWindow,
|
||
screenshot_manager: Optional[AutoScreenshot] = None):
|
||
"""
|
||
Initialize screenshot hotkey manager.
|
||
|
||
Args:
|
||
parent_window: Main window to attach Qt shortcuts to
|
||
screenshot_manager: Existing AutoScreenshot instance
|
||
"""
|
||
super().__init__(parent_window)
|
||
|
||
self.parent_window = parent_window
|
||
self.screenshot_manager = screenshot_manager or AutoScreenshot()
|
||
self.hotkeys: Dict[str, str] = self.DEFAULT_HOTKEYS.copy()
|
||
self.enabled = True
|
||
|
||
# Qt shortcuts (always work when app is focused)
|
||
self._qt_shortcuts: Dict[str, QShortcut] = {}
|
||
|
||
# Global hotkey hooks (work even when app not focused, Windows only)
|
||
self._global_hooks: Dict[str, Callable] = {}
|
||
self._global_enabled = False
|
||
|
||
# Callbacks for custom actions
|
||
self.on_screenshot: Optional[Callable[[ScreenshotEvent], None]] = None
|
||
|
||
# Screenshot history
|
||
self.recent_screenshots: List[ScreenshotEvent] = []
|
||
self.max_history = 50
|
||
|
||
# Initialize Qt shortcuts
|
||
self._setup_qt_shortcuts()
|
||
|
||
# Try to setup global hotkeys
|
||
self._setup_global_hotkeys()
|
||
|
||
def _setup_qt_shortcuts(self):
|
||
"""Setup Qt keyboard shortcuts (works when app is focused)."""
|
||
try:
|
||
for action, combo in self.hotkeys.items():
|
||
qt_sequence = self._to_qt_sequence(combo)
|
||
shortcut = QShortcut(qt_sequence, self.parent_window)
|
||
shortcut.activated.connect(lambda a=action: self._on_hotkey(a))
|
||
self._qt_shortcuts[action] = shortcut
|
||
logger.info(f"Qt shortcut registered: {combo} -> {action}")
|
||
except Exception as e:
|
||
logger.error(f"Failed to setup Qt shortcuts: {e}")
|
||
|
||
def _to_qt_sequence(self, combo: str) -> QKeySequence:
|
||
"""Convert our combo format to Qt key sequence."""
|
||
# Convert to Qt format
|
||
qt_combo = combo.replace('Ctrl', 'Ctrl').replace('Shift', 'Shift').replace('Alt', 'Alt')
|
||
return QKeySequence(qt_combo)
|
||
|
||
def _setup_global_hotkeys(self) -> bool:
|
||
"""
|
||
Setup global hotkeys using 'keyboard' library.
|
||
Works even when app is not focused (requires admin on Windows).
|
||
"""
|
||
if not KEYBOARD_LIB_AVAILABLE:
|
||
logger.info("'keyboard' library not installed. Qt shortcuts only.")
|
||
return False
|
||
|
||
try:
|
||
# Unhook all first to be safe
|
||
keyboard.unhook_all()
|
||
|
||
for action, combo in self.hotkeys.items():
|
||
try:
|
||
# Convert to keyboard library format
|
||
kb_combo = combo.lower().replace('ctrl', 'ctrl').replace('shift', 'shift').replace('alt', 'alt')
|
||
|
||
# Register hotkey
|
||
keyboard.add_hotkey(kb_combo, lambda a=action: self._on_hotkey(a))
|
||
logger.info(f"Global hotkey registered: {kb_combo}")
|
||
|
||
except Exception as e:
|
||
logger.warning(f"Failed to register global hotkey '{combo}': {e}")
|
||
|
||
self._global_enabled = True
|
||
logger.info("Global hotkeys enabled (requires admin for full functionality)")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.warning(f"Global hotkeys not available: {e}")
|
||
logger.info("Using Qt shortcuts only (app must be focused)")
|
||
return False
|
||
|
||
def is_available(self) -> bool:
|
||
"""Check if hotkey manager is available."""
|
||
return len(self._qt_shortcuts) > 0
|
||
|
||
def is_global_available(self) -> bool:
|
||
"""Check if global hotkeys are working."""
|
||
return self._global_enabled
|
||
|
||
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 center region."""
|
||
filename = f"manual_region_{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']
|
||
|
||
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"
|
||
|
||
try:
|
||
import mss
|
||
with mss.mss() as sct:
|
||
monitor = sct.monitors[1]
|
||
screen_w = monitor['width']
|
||
screen_h = monitor['height']
|
||
|
||
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']
|
||
|
||
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)
|
||
|
||
if len(self.recent_screenshots) > self.max_history:
|
||
self.recent_screenshots = self.recent_screenshots[:self.max_history]
|
||
|
||
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:
|
||
# Update Qt shortcut
|
||
if action in self._qt_shortcuts:
|
||
self._qt_shortcuts[action].setKey(QKeySequence(combination))
|
||
|
||
# Update global hotkey if enabled
|
||
if self._global_enabled and KEYBOARD_LIB_AVAILABLE:
|
||
keyboard.unhook_all()
|
||
self.hotkeys[action] = combination
|
||
self._setup_global_hotkeys()
|
||
else:
|
||
self.hotkeys[action] = combination
|
||
|
||
logger.info(f"Hotkey for '{action}' set to '{combination}'")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to set hotkey '{combination}': {e}")
|
||
return False
|
||
|
||
def get_hotkey_help(self) -> str:
|
||
"""Get formatted hotkey help text."""
|
||
lines = ["📸 Screenshot Hotkeys:", ""]
|
||
|
||
if self._global_enabled:
|
||
lines.append("✅ Global hotkeys active (work even when game is focused)")
|
||
else:
|
||
lines.append("ℹ️ Qt shortcuts only (app must be focused)")
|
||
lines.append(" For global hotkeys, run as Administrator or install 'keyboard' library")
|
||
|
||
lines.append("")
|
||
|
||
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:<15} - {desc}")
|
||
|
||
return "\n".join(lines)
|
||
|
||
def stop(self):
|
||
"""Stop hotkey listeners."""
|
||
# Remove global hooks
|
||
if self._global_enabled and KEYBOARD_LIB_AVAILABLE:
|
||
try:
|
||
keyboard.unhook_all()
|
||
logger.info("Global hotkeys unregistered")
|
||
except:
|
||
pass
|
||
|
||
# Qt shortcuts are automatically cleaned up
|
||
logger.info("Screenshot hotkeys stopped")
|
||
|
||
|
||
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()
|
||
|
||
# Status label
|
||
status_label = QLabel()
|
||
if self.manager.is_global_available():
|
||
status_label.setText("✅ Global hotkeys active - work even when game is focused")
|
||
status_label.setStyleSheet("color: #4caf50;")
|
||
else:
|
||
status_label.setText("ℹ️ Qt shortcuts only - app must be focused\n"
|
||
"For global hotkeys, run as Administrator")
|
||
status_label.setStyleSheet("color: #ff9800;")
|
||
layout.addWidget(status_label)
|
||
|
||
# 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()
|
||
if combo:
|
||
self.manager.set_hotkey(action, combo)
|
||
|
||
|
||
# Convenience function
|
||
def create_screenshot_hotkeys(parent_window: QMainWindow,
|
||
screenshot_manager: Optional[AutoScreenshot] = None) \
|
||
-> Optional[ScreenshotHotkeyManager]:
|
||
"""
|
||
Create screenshot hotkey manager.
|
||
|
||
Args:
|
||
parent_window: Main window to attach shortcuts to
|
||
screenshot_manager: Existing AutoScreenshot instance
|
||
|
||
Returns:
|
||
ScreenshotHotkeyManager or None if not available
|
||
"""
|
||
try:
|
||
manager = ScreenshotHotkeyManager(parent_window, screenshot_manager)
|
||
if manager.is_available():
|
||
return manager
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f"Failed to create screenshot hotkeys: {e}")
|
||
return None |