254 lines
8.0 KiB
Python
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
|