diff --git a/plugins/base_plugin.py b/plugins/base_plugin.py index 6467a9c..8934498 100644 --- a/plugins/base_plugin.py +++ b/plugins/base_plugin.py @@ -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 diff --git a/plugins/settings/plugin.py b/plugins/settings/plugin.py index 4cdc917..59659ac 100644 --- a/plugins/settings/plugin.py +++ b/plugins/settings/plugin.py @@ -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!")