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):
|
||||
"""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
|
||||
name: str = "Unnamed Plugin"
|
||||
|
|
@ -25,7 +46,8 @@ class BasePlugin(ABC):
|
|||
icon: Optional[str] = None
|
||||
|
||||
# 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
|
||||
|
||||
# Dependencies - override in subclass
|
||||
|
|
|
|||
|
|
@ -518,32 +518,118 @@ class SettingsPlugin(BasePlugin):
|
|||
dialog.exec()
|
||||
|
||||
def _create_hotkeys_tab(self):
|
||||
"""Create hotkeys configuration tab."""
|
||||
"""Create hotkeys configuration tab - dynamically discovers hotkeys from plugins."""
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
layout.setSpacing(15)
|
||||
|
||||
hotkeys_group = QGroupBox("Global Hotkeys")
|
||||
hotkeys_group.setStyleSheet(self._group_style())
|
||||
hotkeys_layout = QVBoxLayout(hotkeys_group)
|
||||
# Info label
|
||||
info = QLabel("Hotkeys are advertised by plugins. Changes apply on next restart.")
|
||||
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 = {}
|
||||
|
||||
hotkeys = [
|
||||
("Toggle Overlay", "hotkey_toggle", "ctrl+shift+u"),
|
||||
("Universal Search", "hotkey_search", "ctrl+shift+f"),
|
||||
("Calculator", "hotkey_calculator", "ctrl+shift+c"),
|
||||
("Spotify", "hotkey_music", "ctrl+shift+m"),
|
||||
("Game Reader", "hotkey_scan", "ctrl+shift+r"),
|
||||
("Skill Scanner", "hotkey_skills", "ctrl+shift+s"),
|
||||
# Collect hotkeys from all plugins
|
||||
plugin_hotkeys = self._collect_plugin_hotkeys()
|
||||
|
||||
# Group by plugin
|
||||
for plugin_name, hotkeys in sorted(plugin_hotkeys.items()):
|
||||
# Plugin group
|
||||
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.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.setText(self.settings.get(key, default))
|
||||
current = self.settings.get(config_key, default)
|
||||
input_field.setText(current)
|
||||
input_field.setPlaceholderText(default)
|
||||
input_field.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background-color: rgba(30, 35, 45, 200);
|
||||
|
|
@ -553,17 +639,102 @@ class SettingsPlugin(BasePlugin):
|
|||
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)
|
||||
|
||||
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()
|
||||
hotkeys_layout.addLayout(row)
|
||||
core_layout.addLayout(row)
|
||||
|
||||
layout.addWidget(hotkeys_group)
|
||||
layout.addStretch()
|
||||
hotkeys_layout.addWidget(core_group)
|
||||
hotkeys_layout.addStretch()
|
||||
|
||||
scroll.setWidget(scroll_content)
|
||||
layout.addWidget(scroll)
|
||||
|
||||
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):
|
||||
"""Create overlay widgets configuration tab."""
|
||||
tab = QWidget()
|
||||
|
|
@ -698,9 +869,14 @@ class SettingsPlugin(BasePlugin):
|
|||
self.settings.set('minimize_to_tray', self.minimize_cb.isChecked())
|
||||
self.settings.set('show_tooltips', self.tooltips_cb.isChecked())
|
||||
|
||||
# Hotkeys
|
||||
for key, input_field in self.hotkey_inputs.items():
|
||||
self.settings.set(key, input_field.text())
|
||||
# Hotkeys - new structure with dict values
|
||||
for config_key, hotkey_data in self.hotkey_inputs.items():
|
||||
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!")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue