feat: Enhanced Plugin Management UI with Dependency Visualization
NEW FEATURES: 1. Dependency Indicators: - 🔗 (cyan) - Plugin has dependencies (hover for list) - ⚠️ (yellow) - Plugin is required by other enabled plugins - 🔄 (orange) - Auto-enabled due to dependency 2. Legend Bar: Shows what each indicator means at the top of the plugins tab 3. Dependency Tooltips: - 🔗 Shows: 'This plugin requires: X, Y, Z. These will be auto-enabled.' - ⚠️ Shows: 'Required by enabled plugins: A, B. Disable those first.' - 🔄 Shows: 'Auto-enabled by: PluginName' 4. Enable with Dependencies: When enabling a plugin with unmet dependencies: - Shows confirmation dialog listing all plugins to be enabled - User can cancel before enabling - Dependencies are auto-enabled on confirmation 5. Disable Protection: When trying to disable a plugin that others depend on: - Shows warning dialog listing dependent plugins - Prevents accidental breaking of dependencies - User must disable dependents first 6. Dependency Report Dialog: - New '📋 Dependency Report' button - Shows HTML report with: * Summary stats (total/enabled plugins) * Plugins with dependencies list * Plugins required by others list * Full dependency chains 7. Enable/Disable All with Ordering: - Dependencies are enabled first (topological sort) - Dependents are disabled first (reverse order) - Prevents enable/disable failures due to ordering 8. Auto-refresh UI: After enabling/disabling, plugin list refreshes to show: - Updated auto-enabled status - Updated dependency indicators - Updated checkbox states VISUAL IMPROVEMENTS: - Better spacing and layout - Color-coded indicators - Clear visual hierarchy - Informative tooltips throughout This makes plugin management much more intuitive and prevents common mistakes like accidentally breaking dependencies.
This commit is contained in:
parent
194cda2c62
commit
b63763b528
|
|
@ -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()
|
||||
|
||||
# Notify user a restart might be needed for some changes
|
||||
# But we try to apply immediately
|
||||
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 = []
|
||||
|
||||
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 "<p>No plugin manager available</p>"
|
||||
|
||||
plugin_manager = self.overlay.plugin_manager
|
||||
all_plugins = plugin_manager.get_all_discovered_plugins()
|
||||
|
||||
html = ["<html><body style='font-family: Arial; color: white;'>"]
|
||||
html.append("<h2>📋 Plugin Dependency Report</h2>")
|
||||
html.append("<hr>")
|
||||
|
||||
# Summary section
|
||||
total = len(all_plugins)
|
||||
enabled = sum(1 for pid in all_plugins if plugin_manager.is_plugin_enabled(pid))
|
||||
html.append(f"<p><b>Total Plugins:</b> {total} | <b>Enabled:</b> {enabled}</p>")
|
||||
html.append("<hr>")
|
||||
|
||||
# Plugins with dependencies
|
||||
html.append("<h3>🔗 Plugins with Dependencies</h3>")
|
||||
html.append("<ul>")
|
||||
|
||||
for plugin_id, deps in sorted(self.plugin_deps.items()):
|
||||
if deps:
|
||||
plugin_class = all_plugins.get(plugin_id)
|
||||
if plugin_class:
|
||||
name = plugin_class.name
|
||||
dep_names = [d.split('.')[-1].replace('_', ' ').title() for d in deps]
|
||||
html.append(f"<li><b>{name}</b> requires: {', '.join(dep_names)}</li>")
|
||||
|
||||
html.append("</ul>")
|
||||
html.append("<hr>")
|
||||
|
||||
# Plugins required by others
|
||||
html.append("<h3>⚠️ Plugins Required by Others</h3>")
|
||||
html.append("<ul>")
|
||||
|
||||
for plugin_id, dependents in sorted(self.plugin_dependents.items()):
|
||||
if dependents:
|
||||
plugin_class = all_plugins.get(plugin_id)
|
||||
if plugin_class:
|
||||
name = plugin_class.name
|
||||
dep_names = [d.split('.')[-1].replace('_', ' ').title() for d in dependents]
|
||||
html.append(f"<li><b>{name}</b> is required by: {', '.join(dep_names)}</li>")
|
||||
|
||||
html.append("</ul>")
|
||||
html.append("<hr>")
|
||||
|
||||
# Dependency chain visualization
|
||||
html.append("<h3>🔄 Dependency Chains</h3>")
|
||||
html.append("<ul>")
|
||||
|
||||
for plugin_id, plugin_class in sorted(all_plugins.items(), key=lambda x: x[1].name):
|
||||
chain = self._get_dependency_chain(plugin_id)
|
||||
if len(chain) > 1:
|
||||
chain_names = [all_plugins.get(pid, type('obj', (object,), {'name': pid})) .name for pid in chain]
|
||||
html.append(f"<li>{' → '.join(chain_names)}</li>")
|
||||
|
||||
html.append("</ul>")
|
||||
html.append("</body></html>")
|
||||
|
||||
return "\n".join(html)
|
||||
|
||||
def _get_dependency_chain(self, plugin_id: str, visited=None) -> list:
|
||||
"""Get the dependency chain for a plugin."""
|
||||
if visited is None:
|
||||
visited = set()
|
||||
|
||||
if plugin_id in visited:
|
||||
return [plugin_id] # Circular dependency
|
||||
|
||||
visited.add(plugin_id)
|
||||
chain = [plugin_id]
|
||||
|
||||
# Get what this plugin depends on
|
||||
deps = self.plugin_deps.get(plugin_id, [])
|
||||
for dep_id in deps:
|
||||
if dep_id not in visited:
|
||||
chain.extend(self._get_dependency_chain(dep_id, visited))
|
||||
|
||||
return chain
|
||||
|
||||
def _enable_all_plugins(self):
|
||||
"""Enable all plugins."""
|
||||
"""Enable all plugins with dependency resolution."""
|
||||
if not hasattr(self.overlay, 'plugin_manager'):
|
||||
return
|
||||
|
||||
plugin_manager = self.overlay.plugin_manager
|
||||
for plugin_id, cb in self.plugin_checkboxes.items():
|
||||
cb.setChecked(True)
|
||||
plugin_manager.enable_plugin(plugin_id)
|
||||
all_plugins = plugin_manager.get_all_discovered_plugins()
|
||||
|
||||
# Sort plugins so dependencies are enabled first
|
||||
sorted_plugins = self._sort_plugins_by_dependencies(all_plugins)
|
||||
|
||||
enabled_count = 0
|
||||
for plugin_id in sorted_plugins:
|
||||
cb = self.plugin_checkboxes.get(plugin_id)
|
||||
if cb:
|
||||
cb.setChecked(True)
|
||||
success = plugin_manager.enable_plugin(plugin_id)
|
||||
if success:
|
||||
enabled_count += 1
|
||||
|
||||
self._refresh_plugin_list()
|
||||
print(f"[Settings] Enabled {enabled_count} plugins")
|
||||
|
||||
def _sort_plugins_by_dependencies(self, all_plugins: dict) -> list:
|
||||
"""Sort plugins so dependencies come before dependents."""
|
||||
plugin_ids = list(all_plugins.keys())
|
||||
|
||||
# Build dependency graph
|
||||
graph = {pid: set(self.plugin_deps.get(pid, [])) for pid in plugin_ids}
|
||||
|
||||
# Topological sort
|
||||
sorted_list = []
|
||||
visited = set()
|
||||
temp_mark = set()
|
||||
|
||||
def visit(pid):
|
||||
if pid in temp_mark:
|
||||
return # Circular dependency, skip
|
||||
if pid in visited:
|
||||
return
|
||||
|
||||
temp_mark.add(pid)
|
||||
for dep in graph.get(pid, set()):
|
||||
if dep in graph:
|
||||
visit(dep)
|
||||
temp_mark.remove(pid)
|
||||
visited.add(pid)
|
||||
sorted_list.append(pid)
|
||||
|
||||
for pid in plugin_ids:
|
||||
visit(pid)
|
||||
|
||||
return sorted_list
|
||||
|
||||
def _disable_all_plugins(self):
|
||||
"""Disable all plugins."""
|
||||
"""Disable all plugins in reverse dependency order."""
|
||||
if not hasattr(self.overlay, 'plugin_manager'):
|
||||
return
|
||||
|
||||
plugin_manager = self.overlay.plugin_manager
|
||||
for plugin_id, cb in self.plugin_checkboxes.items():
|
||||
cb.setChecked(False)
|
||||
plugin_manager.disable_plugin(plugin_id)
|
||||
all_plugins = plugin_manager.get_all_discovered_plugins()
|
||||
|
||||
# Sort so dependents are disabled first (reverse of enable order)
|
||||
sorted_plugins = self._sort_plugins_by_dependencies(all_plugins)
|
||||
sorted_plugins.reverse()
|
||||
|
||||
disabled_count = 0
|
||||
for plugin_id in sorted_plugins:
|
||||
cb = self.plugin_checkboxes.get(plugin_id)
|
||||
if cb:
|
||||
cb.setChecked(False)
|
||||
success = plugin_manager.disable_plugin(plugin_id)
|
||||
if success:
|
||||
disabled_count += 1
|
||||
|
||||
self._refresh_plugin_list()
|
||||
print(f"[Settings] Disabled {disabled_count} plugins")
|
||||
|
|
|
|||
Loading…
Reference in New Issue