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