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
|
return tab
|
||||||
|
|
||||||
def _create_plugins_tab(self):
|
def _create_plugins_tab(self):
|
||||||
"""Create plugins management tab - enable/disable plugins."""
|
"""Create plugins management tab with dependency visualization."""
|
||||||
tab = QWidget()
|
tab = QWidget()
|
||||||
layout = QVBoxLayout(tab)
|
layout = QVBoxLayout(tab)
|
||||||
layout.setSpacing(15)
|
layout.setSpacing(15)
|
||||||
|
|
||||||
# Info label
|
# 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);")
|
info.setStyleSheet("color: rgba(255,255,255,150);")
|
||||||
layout.addWidget(info)
|
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 area for plugin list
|
||||||
scroll = QScrollArea()
|
scroll = QScrollArea()
|
||||||
scroll.setWidgetResizable(True)
|
scroll.setWidgetResizable(True)
|
||||||
|
|
@ -206,51 +231,23 @@ class SettingsPlugin(BasePlugin):
|
||||||
plugins_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
plugins_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
self.plugin_checkboxes = {}
|
self.plugin_checkboxes = {}
|
||||||
|
self.plugin_dependency_labels = {}
|
||||||
|
self.plugin_rows = {}
|
||||||
|
|
||||||
# Get all discovered plugins from plugin manager
|
# Get all discovered plugins from plugin manager
|
||||||
if hasattr(self.overlay, 'plugin_manager'):
|
if hasattr(self.overlay, 'plugin_manager'):
|
||||||
plugin_manager = self.overlay.plugin_manager
|
plugin_manager = self.overlay.plugin_manager
|
||||||
all_plugins = plugin_manager.get_all_discovered_plugins()
|
all_plugins = plugin_manager.get_all_discovered_plugins()
|
||||||
|
|
||||||
|
# Build dependency maps
|
||||||
|
self._build_dependency_maps(all_plugins)
|
||||||
|
|
||||||
# Sort by name
|
# Sort by name
|
||||||
sorted_plugins = sorted(all_plugins.items(), key=lambda x: x[1].name)
|
sorted_plugins = sorted(all_plugins.items(), key=lambda x: x[1].name)
|
||||||
|
|
||||||
for plugin_id, plugin_class in sorted_plugins:
|
for plugin_id, plugin_class in sorted_plugins:
|
||||||
row = QHBoxLayout()
|
plugin_row = self._create_plugin_row(plugin_id, plugin_class, plugin_manager)
|
||||||
|
plugins_layout.addLayout(plugin_row)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
sep = QFrame()
|
sep = QFrame()
|
||||||
|
|
@ -292,11 +289,234 @@ class SettingsPlugin(BasePlugin):
|
||||||
disable_all_btn.clicked.connect(self._disable_all_plugins)
|
disable_all_btn.clicked.connect(self._disable_all_plugins)
|
||||||
btn_layout.addWidget(disable_all_btn)
|
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()
|
btn_layout.addStretch()
|
||||||
layout.addLayout(btn_layout)
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
return tab
|
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):
|
def _create_hotkeys_tab(self):
|
||||||
"""Create hotkeys configuration tab."""
|
"""Create hotkeys configuration tab."""
|
||||||
tab = QWidget()
|
tab = QWidget()
|
||||||
|
|
@ -520,42 +740,259 @@ class SettingsPlugin(BasePlugin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _toggle_plugin(self, plugin_id: str, enable: bool):
|
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'):
|
if not hasattr(self.overlay, 'plugin_manager'):
|
||||||
return
|
return
|
||||||
|
|
||||||
plugin_manager = self.overlay.plugin_manager
|
plugin_manager = self.overlay.plugin_manager
|
||||||
|
|
||||||
if enable:
|
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)
|
success = plugin_manager.enable_plugin(plugin_id)
|
||||||
if success:
|
if success:
|
||||||
print(f"[Settings] Enabled plugin: {plugin_id}")
|
print(f"[Settings] Enabled plugin: {plugin_id}")
|
||||||
|
# Refresh UI to show auto-enabled plugins
|
||||||
|
self._refresh_plugin_list()
|
||||||
else:
|
else:
|
||||||
print(f"[Settings] Failed to enable plugin: {plugin_id}")
|
print(f"[Settings] Failed to enable plugin: {plugin_id}")
|
||||||
else:
|
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)
|
success = plugin_manager.disable_plugin(plugin_id)
|
||||||
if success:
|
if success:
|
||||||
print(f"[Settings] Disabled plugin: {plugin_id}")
|
print(f"[Settings] Disabled plugin: {plugin_id}")
|
||||||
|
self._refresh_plugin_list()
|
||||||
|
|
||||||
# Notify user a restart might be needed for some changes
|
def _get_missing_dependencies(self, plugin_id: str, plugin_manager) -> list:
|
||||||
# But we try to apply immediately
|
"""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):
|
def _enable_all_plugins(self):
|
||||||
"""Enable all plugins."""
|
"""Enable all plugins with dependency resolution."""
|
||||||
if not hasattr(self.overlay, 'plugin_manager'):
|
if not hasattr(self.overlay, 'plugin_manager'):
|
||||||
return
|
return
|
||||||
|
|
||||||
plugin_manager = self.overlay.plugin_manager
|
plugin_manager = self.overlay.plugin_manager
|
||||||
for plugin_id, cb in self.plugin_checkboxes.items():
|
all_plugins = plugin_manager.get_all_discovered_plugins()
|
||||||
cb.setChecked(True)
|
|
||||||
plugin_manager.enable_plugin(plugin_id)
|
# 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):
|
def _disable_all_plugins(self):
|
||||||
"""Disable all plugins."""
|
"""Disable all plugins in reverse dependency order."""
|
||||||
if not hasattr(self.overlay, 'plugin_manager'):
|
if not hasattr(self.overlay, 'plugin_manager'):
|
||||||
return
|
return
|
||||||
|
|
||||||
plugin_manager = self.overlay.plugin_manager
|
plugin_manager = self.overlay.plugin_manager
|
||||||
for plugin_id, cb in self.plugin_checkboxes.items():
|
all_plugins = plugin_manager.get_all_discovered_plugins()
|
||||||
cb.setChecked(False)
|
|
||||||
plugin_manager.disable_plugin(plugin_id)
|
# 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