EU-Utility/core/hotkey_manager.py

254 lines
8.0 KiB
Python

"""
EU-Utility - Configurable Hotkey Manager
Manages global and local hotkeys with user customization.
"""
import json
from pathlib import Path
from typing import Dict, Callable, Optional, List
from dataclasses import dataclass, asdict
from enum import Enum
class HotkeyScope(Enum):
"""Scope of hotkey action."""
GLOBAL = "global" # Works even when app not focused
LOCAL = "local" # Only when app is focused
OVERLAY = "overlay" # Only when overlay is visible
@dataclass
class HotkeyConfig:
"""Configuration for a single hotkey."""
action: str
keys: str
scope: str
enabled: bool = True
description: str = ""
def to_dict(self) -> dict:
return asdict(self)
@classmethod
def from_dict(cls, data: dict) -> 'HotkeyConfig':
return cls(**data)
class HotkeyManager:
"""Manages configurable hotkeys for EU-Utility."""
DEFAULT_HOTKEYS = {
'toggle_overlay': HotkeyConfig(
action='toggle_overlay',
keys='ctrl+shift+u',
scope='global',
enabled=True,
description='Toggle main overlay window'
),
'hide_overlays': HotkeyConfig(
action='hide_overlays',
keys='ctrl+shift+h',
scope='global',
enabled=True,
description='Hide all overlays'
),
'quick_search': HotkeyConfig(
action='quick_search',
keys='ctrl+shift+f',
scope='global',
enabled=True,
description='Open universal search'
),
'nexus_search': HotkeyConfig(
action='nexus_search',
keys='ctrl+shift+n',
scope='global',
enabled=True,
description='Open Nexus search'
),
'calculator': HotkeyConfig(
action='calculator',
keys='ctrl+shift+c',
scope='global',
enabled=True,
description='Open calculator'
),
'spotify': HotkeyConfig(
action='spotify',
keys='ctrl+shift+m',
scope='global',
enabled=True,
description='Toggle Spotify controller'
),
'game_reader': HotkeyConfig(
action='game_reader',
keys='ctrl+shift+r',
scope='global',
enabled=True,
description='Open game reader (OCR)'
),
'skill_scanner': HotkeyConfig(
action='skill_scanner',
keys='ctrl+shift+s',
scope='global',
enabled=True,
description='Open skill scanner'
),
'loot_tracker': HotkeyConfig(
action='loot_tracker',
keys='ctrl+shift+l',
scope='global',
enabled=True,
description='Open loot tracker'
),
'screenshot': HotkeyConfig(
action='screenshot',
keys='ctrl+shift+p',
scope='global',
enabled=True,
description='Take screenshot'
),
'close_overlay': HotkeyConfig(
action='close_overlay',
keys='esc',
scope='overlay',
enabled=True,
description='Close overlay (when visible)'
),
'toggle_theme': HotkeyConfig(
action='toggle_theme',
keys='ctrl+t',
scope='local',
enabled=True,
description='Toggle dark/light theme'
),
}
def __init__(self, config_path: str = "config/hotkeys.json"):
self.config_path = Path(config_path)
self.hotkeys: Dict[str, HotkeyConfig] = {}
self._handlers: Dict[str, Callable] = {}
self._registered_globals: List[str] = []
self._load_config()
def _load_config(self):
"""Load hotkey configuration from file."""
if self.config_path.exists():
try:
with open(self.config_path, 'r') as f:
data = json.load(f)
for key, config_dict in data.items():
self.hotkeys[key] = HotkeyConfig.from_dict(config_dict)
# Add any missing defaults
for key, default in self.DEFAULT_HOTKEYS.items():
if key not in self.hotkeys:
self.hotkeys[key] = default
except Exception as e:
print(f"[HotkeyManager] Failed to load config: {e}")
self.hotkeys = self.DEFAULT_HOTKEYS.copy()
else:
self.hotkeys = self.DEFAULT_HOTKEYS.copy()
self._save_config()
def _save_config(self):
"""Save hotkey configuration to file."""
try:
self.config_path.parent.mkdir(parents=True, exist_ok=True)
data = {k: v.to_dict() for k, v in self.hotkeys.items()}
with open(self.config_path, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"[HotkeyManager] Failed to save config: {e}")
def get_hotkey(self, action: str) -> Optional[HotkeyConfig]:
"""Get hotkey configuration for an action."""
return self.hotkeys.get(action)
def set_hotkey(self, action: str, keys: str, scope: str = None, enabled: bool = None):
"""Update hotkey configuration."""
if action in self.hotkeys:
config = self.hotkeys[action]
config.keys = keys
if scope is not None:
config.scope = scope
if enabled is not None:
config.enabled = enabled
self._save_config()
return True
return False
def register_handler(self, action: str, handler: Callable):
"""Register a handler function for an action."""
self._handlers[action] = handler
def get_all_hotkeys(self) -> Dict[str, HotkeyConfig]:
"""Get all hotkey configurations."""
return self.hotkeys.copy()
def validate_key_combo(self, keys: str) -> tuple[bool, str]:
"""Validate a key combination string.
Returns:
(is_valid, error_message)
"""
if not keys or not isinstance(keys, str):
return False, "Key combination cannot be empty"
# Check for valid format
parts = keys.lower().split('+')
if len(parts) < 1:
return False, "Invalid key combination format"
# Valid modifiers
modifiers = {'ctrl', 'alt', 'shift', 'cmd', 'win'}
# Valid keys
valid_keys = {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12',
'esc', 'escape', 'tab', 'space', 'enter', 'return', 'backspace', 'delete',
'insert', 'home', 'end', 'pageup', 'pagedown',
'up', 'down', 'left', 'right',
'print', 'scrolllock', 'pause',
}
for part in parts:
part = part.strip()
if part in modifiers:
continue
if part in valid_keys:
continue
return False, f"Invalid key: '{part}'"
return True, ""
def reset_to_defaults(self):
"""Reset all hotkeys to defaults."""
self.hotkeys = self.DEFAULT_HOTKEYS.copy()
self._save_config()
def get_conflicts(self) -> List[tuple]:
"""Find conflicting hotkey combinations."""
conflicts = []
seen = {}
for action, config in self.hotkeys.items():
if not config.enabled:
continue
key = (config.keys.lower(), config.scope)
if key in seen:
conflicts.append((seen[key], action, config.keys))
else:
seen[key] = action
return conflicts