Lemontropia-Suite/modules/screenshot_hotkey.py

485 lines
17 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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