""" 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': '', # Full screen 'screenshot_region': '+', # Region selection 'screenshot_loot': '+', # Loot window area 'screenshot_hud': '+', # 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