diff --git a/plugins/settings/plugin.py b/plugins/settings/plugin.py index 51e509f..4cdc917 100644 --- a/plugins/settings/plugin.py +++ b/plugins/settings/plugin.py @@ -184,16 +184,41 @@ class SettingsPlugin(BasePlugin): return tab def _create_plugins_tab(self): - """Create plugins management tab - enable/disable plugins.""" + """Create plugins management tab with dependency visualization.""" tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(15) # Info label - info = QLabel("Check plugins to enable them. Uncheck to disable. Changes take effect immediately.") + info = QLabel("Manage plugins. Hover over dependency icons to see requirements. Changes take effect immediately.") info.setStyleSheet("color: rgba(255,255,255,150);") layout.addWidget(info) + # Dependency legend + legend_layout = QHBoxLayout() + legend_layout.addWidget(QLabel("Legend:")) + + # Required by others indicator + req_label = QLabel("⚠️ Required") + req_label.setStyleSheet("color: #ffd93d; font-size: 11px;") + req_label.setToolTip("This plugin is required by other enabled plugins") + legend_layout.addWidget(req_label) + + # Has dependencies indicator + dep_label = QLabel("🔗 Has deps") + dep_label.setStyleSheet("color: #4ecdc4; font-size: 11px;") + dep_label.setToolTip("This plugin requires other plugins to function") + legend_layout.addWidget(dep_label) + + # Auto-enabled indicator + auto_label = QLabel("🔄 Auto") + auto_label.setStyleSheet("color: #ff8c42; font-size: 11px;") + auto_label.setToolTip("Auto-enabled due to dependency") + legend_layout.addWidget(auto_label) + + legend_layout.addStretch() + layout.addLayout(legend_layout) + # Scroll area for plugin list scroll = QScrollArea() scroll.setWidgetResizable(True) @@ -206,51 +231,23 @@ class SettingsPlugin(BasePlugin): plugins_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self.plugin_checkboxes = {} + self.plugin_dependency_labels = {} + self.plugin_rows = {} # Get all discovered plugins from plugin manager if hasattr(self.overlay, 'plugin_manager'): plugin_manager = self.overlay.plugin_manager all_plugins = plugin_manager.get_all_discovered_plugins() + # Build dependency maps + self._build_dependency_maps(all_plugins) + # Sort by name sorted_plugins = sorted(all_plugins.items(), key=lambda x: x[1].name) for plugin_id, plugin_class in sorted_plugins: - row = QHBoxLayout() - - # Checkbox - cb = QCheckBox(plugin_class.name) - cb.setChecked(plugin_manager.is_plugin_enabled(plugin_id)) - cb.setStyleSheet(""" - QCheckBox { - color: white; - spacing: 8px; - } - QCheckBox::indicator { - width: 18px; - height: 18px; - } - """) - # Connect to enable/disable - cb.stateChanged.connect( - lambda state, pid=plugin_id: self._toggle_plugin(pid, state == Qt.CheckState.Checked.value) - ) - self.plugin_checkboxes[plugin_id] = cb - row.addWidget(cb) - - # Version - version_label = QLabel(f"v{plugin_class.version}") - version_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;") - row.addWidget(version_label) - - # Description - desc_label = QLabel(f"- {plugin_class.description}") - desc_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;") - desc_label.setWordWrap(True) - row.addWidget(desc_label, 1) - - row.addStretch() - plugins_layout.addLayout(row) + plugin_row = self._create_plugin_row(plugin_id, plugin_class, plugin_manager) + plugins_layout.addLayout(plugin_row) # Separator sep = QFrame() @@ -292,11 +289,234 @@ class SettingsPlugin(BasePlugin): disable_all_btn.clicked.connect(self._disable_all_plugins) btn_layout.addWidget(disable_all_btn) + # Dependency info button + deps_info_btn = QPushButton("📋 Dependency Report") + deps_info_btn.setStyleSheet(""" + QPushButton { + background-color: #4a9eff; + color: white; + padding: 8px 16px; + border: none; + border-radius: 4px; + } + """) + deps_info_btn.clicked.connect(self._show_dependency_report) + btn_layout.addWidget(deps_info_btn) + btn_layout.addStretch() layout.addLayout(btn_layout) return tab + def _build_dependency_maps(self, all_plugins): + """Build maps of plugin dependencies.""" + self.plugin_deps = {} # plugin_id -> list of plugin_ids it depends on + self.plugin_dependents = {} # plugin_id -> list of plugin_ids that depend on it + + for plugin_id, plugin_class in all_plugins.items(): + deps = getattr(plugin_class, 'dependencies', {}) + plugin_deps_list = deps.get('plugins', []) + + self.plugin_deps[plugin_id] = plugin_deps_list + + # Build reverse map + for dep_id in plugin_deps_list: + if dep_id not in self.plugin_dependents: + self.plugin_dependents[dep_id] = [] + self.plugin_dependents[dep_id].append(plugin_id) + + def _create_plugin_row(self, plugin_id, plugin_class, plugin_manager): + """Create a plugin row with dependency indicators.""" + row = QHBoxLayout() + row.setSpacing(10) + + # Checkbox + cb = QCheckBox(plugin_class.name) + is_enabled = plugin_manager.is_plugin_enabled(plugin_id) + is_auto_enabled = plugin_manager.is_auto_enabled(plugin_id) if hasattr(plugin_manager, 'is_auto_enabled') else False + + cb.setChecked(is_enabled) + cb.setStyleSheet(""" + QCheckBox { + color: white; + spacing: 8px; + } + QCheckBox::indicator { + width: 18px; + height: 18px; + } + QCheckBox::indicator:disabled { + background-color: #ff8c42; + } + """) + + # Disable checkbox if auto-enabled + if is_auto_enabled: + cb.setEnabled(False) + cb.setText(f"{plugin_class.name} (auto)") + + # Connect to enable/disable + cb.stateChanged.connect( + lambda state, pid=plugin_id: self._toggle_plugin(pid, state == Qt.CheckState.Checked.value) + ) + self.plugin_checkboxes[plugin_id] = cb + row.addWidget(cb) + + # Dependency indicators + indicators_layout = QHBoxLayout() + indicators_layout.setSpacing(4) + + # Check if this plugin has dependencies + deps = self.plugin_deps.get(plugin_id, []) + if deps: + deps_btn = QPushButton("🔗") + deps_btn.setFixedSize(24, 24) + deps_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #4ecdc4; + border: none; + font-size: 12px; + } + QPushButton:hover { + background-color: rgba(78, 205, 196, 30); + border-radius: 4px; + } + """) + deps_btn.setToolTip(self._format_dependencies_tooltip(plugin_id, deps)) + indicators_layout.addWidget(deps_btn) + + # Check if other plugins depend on this one + dependents = self.plugin_dependents.get(plugin_id, []) + enabled_dependents = [d for d in dependents if plugin_manager.is_plugin_enabled(d)] + if enabled_dependents: + req_btn = QPushButton("⚠️") + req_btn.setFixedSize(24, 24) + req_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #ffd93d; + border: none; + font-size: 12px; + } + QPushButton:hover { + background-color: rgba(255, 217, 61, 30); + border-radius: 4px; + } + """) + req_btn.setToolTip(self._format_dependents_tooltip(plugin_id, enabled_dependents)) + indicators_layout.addWidget(req_btn) + + # Check if auto-enabled + if is_auto_enabled: + auto_btn = QPushButton("🔄") + auto_btn.setFixedSize(24, 24) + auto_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #ff8c42; + border: none; + font-size: 12px; + } + QPushButton:hover { + background-color: rgba(255, 140, 66, 30); + border-radius: 4px; + } + """) + # Find what enabled this plugin + enabler = self._find_enabler(plugin_id, plugin_manager) + auto_btn.setToolTip(f"Auto-enabled by: {enabler or 'dependency resolution'}") + indicators_layout.addWidget(auto_btn) + + row.addLayout(indicators_layout) + + # Version + version_label = QLabel(f"v{plugin_class.version}") + version_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;") + row.addWidget(version_label) + + # Description + desc_label = QLabel(f"- {plugin_class.description}") + desc_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;") + desc_label.setWordWrap(True) + row.addWidget(desc_label, 1) + + row.addStretch() + + # Store row reference for updates + self.plugin_rows[plugin_id] = row + + return row + + def _format_dependencies_tooltip(self, plugin_id, deps): + """Format tooltip for dependencies.""" + lines = ["This plugin requires:"] + for dep_id in deps: + dep_name = dep_id.split('.')[-1].replace('_', ' ').title() + lines.append(f" • {dep_name}") + lines.append("") + lines.append("These will be auto-enabled when you enable this plugin.") + return "\n".join(lines) + + def _format_dependents_tooltip(self, plugin_id, dependents): + """Format tooltip for plugins that depend on this one.""" + lines = ["Required by enabled plugins:"] + for dep_id in dependents: + dep_name = dep_id.split('.')[-1].replace('_', ' ').title() + lines.append(f" • {dep_name}") + lines.append("") + lines.append("Disable these first to disable this plugin.") + return "\n".join(lines) + + def _find_enabler(self, plugin_id, plugin_manager): + """Find which plugin auto-enabled this one.""" + # Check all enabled plugins to see which one depends on this + for other_id, other_class in plugin_manager.get_all_discovered_plugins().items(): + if plugin_manager.is_plugin_enabled(other_id): + deps = getattr(other_class, 'dependencies', {}).get('plugins', []) + if plugin_id in deps: + return other_class.name + return None + + def _show_dependency_report(self): + """Show a dialog with full dependency report.""" + from PyQt6.QtWidgets import QDialog, QTextEdit, QVBoxLayout, QPushButton + + dialog = QDialog() + dialog.setWindowTitle("Plugin Dependency Report") + dialog.setMinimumSize(600, 400) + dialog.setStyleSheet(""" + QDialog { + background-color: #1a1f2e; + } + QTextEdit { + background-color: #232837; + color: white; + border: 1px solid rgba(100, 110, 130, 80); + padding: 10px; + } + QPushButton { + background-color: #4a9eff; + color: white; + padding: 8px 16px; + border: none; + border-radius: 4px; + } + """) + + layout = QVBoxLayout(dialog) + + text_edit = QTextEdit() + text_edit.setReadOnly(True) + text_edit.setHtml(self._generate_dependency_report()) + layout.addWidget(text_edit) + + close_btn = QPushButton("Close") + close_btn.clicked.connect(dialog.close) + layout.addWidget(close_btn) + + dialog.exec() + def _create_hotkeys_tab(self): """Create hotkeys configuration tab.""" tab = QWidget() @@ -520,42 +740,259 @@ class SettingsPlugin(BasePlugin): pass def _toggle_plugin(self, plugin_id: str, enable: bool): - """Enable or disable a plugin.""" + """Enable or disable a plugin with dependency handling.""" if not hasattr(self.overlay, 'plugin_manager'): return plugin_manager = self.overlay.plugin_manager if enable: + # Get dependencies that will be auto-enabled + deps_to_enable = self._get_missing_dependencies(plugin_id, plugin_manager) + + if deps_to_enable: + # Show confirmation dialog + from PyQt6.QtWidgets import QMessageBox + + dep_names = [pid.split('.')[-1].replace('_', ' ').title() for pid in deps_to_enable] + msg = f"Enabling this plugin will also enable:\n\n" + msg += "\n".join(f" • {name}" for name in dep_names) + msg += "\n\nContinue?" + + reply = QMessageBox.question( + None, "Enable Dependencies", msg, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply != QMessageBox.StandardButton.Yes: + # Uncheck the box + self.plugin_checkboxes[plugin_id].setChecked(False) + return + success = plugin_manager.enable_plugin(plugin_id) if success: print(f"[Settings] Enabled plugin: {plugin_id}") + # Refresh UI to show auto-enabled plugins + self._refresh_plugin_list() else: print(f"[Settings] Failed to enable plugin: {plugin_id}") else: + # Check if other enabled plugins depend on this one + dependents = self.plugin_dependents.get(plugin_id, []) + enabled_dependents = [d for d in dependents if plugin_manager.is_plugin_enabled(d)] + + if enabled_dependents: + # Show warning + from PyQt6.QtWidgets import QMessageBox + + dep_names = [pid.split('.')[-1].replace('_', ' ').title() for pid in enabled_dependents] + msg = f"Cannot disable: This plugin is required by:\n\n" + msg += "\n".join(f" • {name}" for name in dep_names) + msg += "\n\nDisable those plugins first." + + QMessageBox.warning(None, "Dependency Warning", msg) + + # Recheck the box + self.plugin_checkboxes[plugin_id].setChecked(True) + return + success = plugin_manager.disable_plugin(plugin_id) if success: print(f"[Settings] Disabled plugin: {plugin_id}") + self._refresh_plugin_list() + + def _get_missing_dependencies(self, plugin_id: str, plugin_manager) -> list: + """Get list of dependencies that need to be enabled.""" + deps = self.plugin_deps.get(plugin_id, []) + missing = [] - # Notify user a restart might be needed for some changes - # But we try to apply immediately + for dep_id in deps: + if not plugin_manager.is_plugin_enabled(dep_id): + missing.append(dep_id) + + return missing + + def _refresh_plugin_list(self): + """Refresh the plugin list UI.""" + if not hasattr(self.overlay, 'plugin_manager'): + return + + plugin_manager = self.overlay.plugin_manager + + for plugin_id, cb in self.plugin_checkboxes.items(): + is_enabled = plugin_manager.is_plugin_enabled(plugin_id) + is_auto_enabled = plugin_manager.is_auto_enabled(plugin_id) if hasattr(plugin_manager, 'is_auto_enabled') else False + + cb.setChecked(is_enabled) + + if is_auto_enabled: + cb.setEnabled(False) + # Update text to show auto status + plugin_class = plugin_manager.get_all_discovered_plugins().get(plugin_id) + if plugin_class: + cb.setText(f"{plugin_class.name} (auto)") + else: + cb.setEnabled(True) + plugin_class = plugin_manager.get_all_discovered_plugins().get(plugin_id) + if plugin_class: + cb.setText(plugin_class.name) + + def _generate_dependency_report(self) -> str: + """Generate HTML dependency report.""" + if not hasattr(self.overlay, 'plugin_manager'): + return "
No plugin manager available
" + + plugin_manager = self.overlay.plugin_manager + all_plugins = plugin_manager.get_all_discovered_plugins() + + html = [""] + html.append("Total Plugins: {total} | Enabled: {enabled}
") + html.append("