feat: Dynamic hotkey discovery from plugins
REFACTOR: Hotkeys are no longer hardcoded in Settings UI
NEW SYSTEM:
1. Plugins advertise their hotkeys via class attributes:
Legacy (single hotkey):
hotkey = 'ctrl+shift+s'
New format (multiple hotkeys with descriptions):
hotkeys = [
{
'action': 'toggle',
'description': 'Toggle Skill Scanner',
'default': 'ctrl+shift+s',
'config_key': 'skillscanner_toggle' # optional
},
{
'action': 'quick_scan',
'description': 'Quick Scan',
'default': 'f12',
}
]
2. Settings UI dynamically discovers hotkeys:
- Scans all plugins for hotkey/hotkeys attributes
- Groups hotkeys by plugin name
- Shows description + input field + reset button
- Core system hotkeys in separate 'Core System' group
3. Visual improvements:
- Scrollable hotkey list
- Reset button (↺) for each hotkey
- Tooltips showing default value
- Plugin grouping for organization
4. Backward compatible:
- Still supports legacy 'hotkey' attribute
- Converts to new format automatically
- Existing settings preserved
BASE PLUGIN:
- Added hotkeys attribute documentation
- Added docstring with usage examples
- Shows both formats in class docstring
SETTINGS PLUGIN:
- _create_hotkeys_tab() now dynamic
- _collect_plugin_hotkeys() scans all plugins
- Groups by plugin with QGroupBox
- Reset buttons restore defaults
- ScrollArea for long lists
This allows plugins to define multiple hotkeys with
descriptions, and they'll automatically appear in
Settings without hardcoding them in the core.
This commit is contained in:
parent
b63763b528
commit
09ad30c223
|
|
@ -15,7 +15,28 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class BasePlugin(ABC):
|
class BasePlugin(ABC):
|
||||||
"""Base class for all EU-Utility plugins."""
|
"""Base class for all EU-Utility plugins.
|
||||||
|
|
||||||
|
To define hotkeys for your plugin, use either:
|
||||||
|
|
||||||
|
1. Legacy single hotkey (simple toggle):
|
||||||
|
hotkey = "ctrl+shift+n"
|
||||||
|
|
||||||
|
2. New multi-hotkey format (recommended):
|
||||||
|
hotkeys = [
|
||||||
|
{
|
||||||
|
'action': 'toggle', # Unique action identifier
|
||||||
|
'description': 'Toggle My Plugin', # Display name in settings
|
||||||
|
'default': 'ctrl+shift+m', # Default hotkey combination
|
||||||
|
'config_key': 'myplugin_toggle' # Settings key (optional)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'action': 'quick_action',
|
||||||
|
'description': 'Quick Scan',
|
||||||
|
'default': 'ctrl+shift+s',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
# Plugin metadata - override in subclass
|
# Plugin metadata - override in subclass
|
||||||
name: str = "Unnamed Plugin"
|
name: str = "Unnamed Plugin"
|
||||||
|
|
@ -25,7 +46,8 @@ class BasePlugin(ABC):
|
||||||
icon: Optional[str] = None
|
icon: Optional[str] = None
|
||||||
|
|
||||||
# Plugin settings
|
# Plugin settings
|
||||||
hotkey: Optional[str] = None # e.g., "ctrl+shift+n"
|
hotkey: Optional[str] = None # Legacy single hotkey (e.g., "ctrl+shift+n")
|
||||||
|
hotkeys: Optional[List[Dict[str, str]]] = None # New multi-hotkey format
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
|
|
||||||
# Dependencies - override in subclass
|
# Dependencies - override in subclass
|
||||||
|
|
|
||||||
|
|
@ -518,32 +518,118 @@ class SettingsPlugin(BasePlugin):
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
|
||||||
def _create_hotkeys_tab(self):
|
def _create_hotkeys_tab(self):
|
||||||
"""Create hotkeys configuration tab."""
|
"""Create hotkeys configuration tab - dynamically discovers hotkeys from plugins."""
|
||||||
tab = QWidget()
|
tab = QWidget()
|
||||||
layout = QVBoxLayout(tab)
|
layout = QVBoxLayout(tab)
|
||||||
layout.setSpacing(15)
|
layout.setSpacing(15)
|
||||||
|
|
||||||
hotkeys_group = QGroupBox("Global Hotkeys")
|
# Info label
|
||||||
hotkeys_group.setStyleSheet(self._group_style())
|
info = QLabel("Hotkeys are advertised by plugins. Changes apply on next restart.")
|
||||||
hotkeys_layout = QVBoxLayout(hotkeys_group)
|
info.setStyleSheet("color: rgba(255,255,255,150);")
|
||||||
|
layout.addWidget(info)
|
||||||
|
|
||||||
|
# Scroll area for hotkeys
|
||||||
|
scroll = QScrollArea()
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
|
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
|
scroll.setStyleSheet("background: transparent; border: none;")
|
||||||
|
|
||||||
|
scroll_content = QWidget()
|
||||||
|
hotkeys_layout = QVBoxLayout(scroll_content)
|
||||||
|
hotkeys_layout.setSpacing(10)
|
||||||
|
hotkeys_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
self.hotkey_inputs = {}
|
self.hotkey_inputs = {}
|
||||||
|
|
||||||
hotkeys = [
|
# Collect hotkeys from all plugins
|
||||||
("Toggle Overlay", "hotkey_toggle", "ctrl+shift+u"),
|
plugin_hotkeys = self._collect_plugin_hotkeys()
|
||||||
("Universal Search", "hotkey_search", "ctrl+shift+f"),
|
|
||||||
("Calculator", "hotkey_calculator", "ctrl+shift+c"),
|
# Group by plugin
|
||||||
("Spotify", "hotkey_music", "ctrl+shift+m"),
|
for plugin_name, hotkeys in sorted(plugin_hotkeys.items()):
|
||||||
("Game Reader", "hotkey_scan", "ctrl+shift+r"),
|
# Plugin group
|
||||||
("Skill Scanner", "hotkey_skills", "ctrl+shift+s"),
|
group = QGroupBox(plugin_name)
|
||||||
|
group.setStyleSheet(self._group_style())
|
||||||
|
group_layout = QVBoxLayout(group)
|
||||||
|
|
||||||
|
for hotkey_info in hotkeys:
|
||||||
|
row = QHBoxLayout()
|
||||||
|
|
||||||
|
# Description
|
||||||
|
desc = hotkey_info.get('description', hotkey_info['action'])
|
||||||
|
desc_label = QLabel(f"{desc}:")
|
||||||
|
desc_label.setStyleSheet("color: white; min-width: 150px;")
|
||||||
|
row.addWidget(desc_label)
|
||||||
|
|
||||||
|
# Hotkey input
|
||||||
|
input_field = QLineEdit()
|
||||||
|
input_field.setText(hotkey_info['current'])
|
||||||
|
input_field.setPlaceholderText(hotkey_info['default'])
|
||||||
|
input_field.setStyleSheet("""
|
||||||
|
QLineEdit {
|
||||||
|
background-color: rgba(30, 35, 45, 200);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(100, 110, 130, 80);
|
||||||
|
padding: 5px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Store reference with config key
|
||||||
|
config_key = hotkey_info['config_key']
|
||||||
|
self.hotkey_inputs[config_key] = {
|
||||||
|
'input': input_field,
|
||||||
|
'default': hotkey_info['default'],
|
||||||
|
'plugin': plugin_name,
|
||||||
|
'action': hotkey_info['action']
|
||||||
|
}
|
||||||
|
|
||||||
|
row.addWidget(input_field)
|
||||||
|
|
||||||
|
# Reset button
|
||||||
|
reset_btn = QPushButton("↺")
|
||||||
|
reset_btn.setFixedSize(28, 28)
|
||||||
|
reset_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: rgba(255,255,255,20);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(255,255,255,40);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
reset_btn.setToolTip(f"Reset to default: {hotkey_info['default']}")
|
||||||
|
reset_btn.clicked.connect(lambda checked, inp=input_field, default=hotkey_info['default']: inp.setText(default))
|
||||||
|
row.addWidget(reset_btn)
|
||||||
|
|
||||||
|
row.addStretch()
|
||||||
|
group_layout.addLayout(row)
|
||||||
|
|
||||||
|
hotkeys_layout.addWidget(group)
|
||||||
|
|
||||||
|
# Core hotkeys section (always present)
|
||||||
|
core_group = QGroupBox("Core System")
|
||||||
|
core_group.setStyleSheet(self._group_style())
|
||||||
|
core_layout = QVBoxLayout(core_group)
|
||||||
|
|
||||||
|
core_hotkeys = [
|
||||||
|
("Toggle Overlay", "hotkey_toggle", "ctrl+shift+u", "Show/hide the EU-Utility overlay"),
|
||||||
|
("Universal Search", "hotkey_search", "ctrl+shift+f", "Quick search across all plugins"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for label, key, default in hotkeys:
|
for label, config_key, default, description in core_hotkeys:
|
||||||
row = QHBoxLayout()
|
row = QHBoxLayout()
|
||||||
row.addWidget(QLabel(label + ":"))
|
|
||||||
|
desc_label = QLabel(f"{label}:")
|
||||||
|
desc_label.setStyleSheet("color: white; min-width: 150px;")
|
||||||
|
row.addWidget(desc_label)
|
||||||
|
|
||||||
input_field = QLineEdit()
|
input_field = QLineEdit()
|
||||||
input_field.setText(self.settings.get(key, default))
|
current = self.settings.get(config_key, default)
|
||||||
|
input_field.setText(current)
|
||||||
|
input_field.setPlaceholderText(default)
|
||||||
input_field.setStyleSheet("""
|
input_field.setStyleSheet("""
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
background-color: rgba(30, 35, 45, 200);
|
background-color: rgba(30, 35, 45, 200);
|
||||||
|
|
@ -553,17 +639,102 @@ class SettingsPlugin(BasePlugin):
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
self.hotkey_inputs[key] = input_field
|
|
||||||
|
self.hotkey_inputs[config_key] = {
|
||||||
|
'input': input_field,
|
||||||
|
'default': default,
|
||||||
|
'plugin': 'Core',
|
||||||
|
'action': label
|
||||||
|
}
|
||||||
|
|
||||||
row.addWidget(input_field)
|
row.addWidget(input_field)
|
||||||
|
|
||||||
|
reset_btn = QPushButton("↺")
|
||||||
|
reset_btn.setFixedSize(28, 28)
|
||||||
|
reset_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: rgba(255,255,255,20);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(255,255,255,40);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
reset_btn.setToolTip(f"Reset to default: {default}")
|
||||||
|
reset_btn.clicked.connect(lambda checked, inp=input_field, default=default: inp.setText(default))
|
||||||
|
row.addWidget(reset_btn)
|
||||||
|
|
||||||
row.addStretch()
|
row.addStretch()
|
||||||
hotkeys_layout.addLayout(row)
|
core_layout.addLayout(row)
|
||||||
|
|
||||||
layout.addWidget(hotkeys_group)
|
hotkeys_layout.addWidget(core_group)
|
||||||
layout.addStretch()
|
hotkeys_layout.addStretch()
|
||||||
|
|
||||||
|
scroll.setWidget(scroll_content)
|
||||||
|
layout.addWidget(scroll)
|
||||||
|
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
def _collect_plugin_hotkeys(self) -> dict:
|
||||||
|
"""Collect hotkeys from all discovered plugins.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping plugin name to list of hotkey info dicts
|
||||||
|
"""
|
||||||
|
plugin_hotkeys = {}
|
||||||
|
|
||||||
|
if not hasattr(self.overlay, 'plugin_manager'):
|
||||||
|
return plugin_hotkeys
|
||||||
|
|
||||||
|
plugin_manager = self.overlay.plugin_manager
|
||||||
|
all_plugins = plugin_manager.get_all_discovered_plugins()
|
||||||
|
|
||||||
|
for plugin_id, plugin_class in all_plugins.items():
|
||||||
|
hotkeys = getattr(plugin_class, 'hotkeys', None)
|
||||||
|
|
||||||
|
if not hotkeys:
|
||||||
|
# Try legacy single hotkey attribute
|
||||||
|
single_hotkey = getattr(plugin_class, 'hotkey', None)
|
||||||
|
if single_hotkey:
|
||||||
|
hotkeys = [{
|
||||||
|
'action': 'toggle',
|
||||||
|
'description': f"Toggle {plugin_class.name}",
|
||||||
|
'default': single_hotkey,
|
||||||
|
'config_key': f"hotkey_{plugin_id.split('.')[-1]}"
|
||||||
|
}]
|
||||||
|
|
||||||
|
if hotkeys:
|
||||||
|
plugin_name = plugin_class.name
|
||||||
|
plugin_hotkeys[plugin_name] = []
|
||||||
|
|
||||||
|
for i, hk in enumerate(hotkeys):
|
||||||
|
# Support both dict format and simple string
|
||||||
|
if isinstance(hk, dict):
|
||||||
|
hotkey_info = {
|
||||||
|
'action': hk.get('action', f'action_{i}'),
|
||||||
|
'description': hk.get('description', hk.get('action', f'Action {i}')),
|
||||||
|
'default': hk.get('default', ''),
|
||||||
|
'config_key': hk.get('config_key', f"hotkey_{plugin_id.split('.')[-1]}_{i}")
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Simple string format - legacy
|
||||||
|
hotkey_info = {
|
||||||
|
'action': f'hotkey_{i}',
|
||||||
|
'description': f"Hotkey {i+1}",
|
||||||
|
'default': str(hk),
|
||||||
|
'config_key': f"hotkey_{plugin_id.split('.')[-1]}_{i}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get current value from settings
|
||||||
|
hotkey_info['current'] = self.settings.get(hotkey_info['config_key'], hotkey_info['default'])
|
||||||
|
|
||||||
|
plugin_hotkeys[plugin_name].append(hotkey_info)
|
||||||
|
|
||||||
|
return plugin_hotkeys
|
||||||
|
|
||||||
def _create_overlay_tab(self):
|
def _create_overlay_tab(self):
|
||||||
"""Create overlay widgets configuration tab."""
|
"""Create overlay widgets configuration tab."""
|
||||||
tab = QWidget()
|
tab = QWidget()
|
||||||
|
|
@ -698,9 +869,14 @@ class SettingsPlugin(BasePlugin):
|
||||||
self.settings.set('minimize_to_tray', self.minimize_cb.isChecked())
|
self.settings.set('minimize_to_tray', self.minimize_cb.isChecked())
|
||||||
self.settings.set('show_tooltips', self.tooltips_cb.isChecked())
|
self.settings.set('show_tooltips', self.tooltips_cb.isChecked())
|
||||||
|
|
||||||
# Hotkeys
|
# Hotkeys - new structure with dict values
|
||||||
for key, input_field in self.hotkey_inputs.items():
|
for config_key, hotkey_data in self.hotkey_inputs.items():
|
||||||
self.settings.set(key, input_field.text())
|
if isinstance(hotkey_data, dict):
|
||||||
|
input_field = hotkey_data['input']
|
||||||
|
self.settings.set(config_key, input_field.text())
|
||||||
|
else:
|
||||||
|
# Legacy format - direct QLineEdit reference
|
||||||
|
self.settings.set(config_key, hotkey_data.text())
|
||||||
|
|
||||||
print("Settings saved!")
|
print("Settings saved!")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue