EU-Utility-Plugins-Repo/plugins/settings/plugin.py

1175 lines
43 KiB
Python

"""
EU-Utility - Settings UI Plugin
Settings menu for configuring EU-Utility.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QCheckBox, QLineEdit, QComboBox,
QSlider, QTabWidget, QGroupBox, QListWidget,
QListWidgetItem, QFrame, QFileDialog, QScrollArea
)
from PyQt6.QtCore import Qt, QTimer
from core.settings import get_settings
from plugins.base_plugin import BasePlugin
class SettingsPlugin(BasePlugin):
"""EU-Utility settings and configuration."""
name = "Settings"
version = "1.0.0"
author = "ImpulsiveFPS"
description = "Configure EU-Utility preferences"
hotkey = "ctrl+shift+comma"
def initialize(self):
"""Setup settings."""
self.settings = get_settings()
def get_ui(self):
"""Create settings UI."""
widget = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget)
layout.setSpacing(10)
layout.setContentsMargins(0, 0, 0, 0)
# Title
title = QLabel("Settings")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Tabs
tabs = QTabWidget()
tabs.setStyleSheet("""
QTabBar::tab {
background-color: rgba(35, 40, 55, 200);
color: rgba(255,255,255,150);
padding: 10px 20px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
QTabBar::tab:selected {
background-color: #ff8c42;
color: white;
font-weight: bold;
}
""")
# General tab
general_tab = self._create_general_tab()
tabs.addTab(general_tab, "General")
# Plugins tab
plugins_tab = self._create_plugins_tab()
tabs.addTab(plugins_tab, "Plugins")
# Hotkeys tab
hotkeys_tab = self._create_hotkeys_tab()
tabs.addTab(hotkeys_tab, "Hotkeys")
# Overlay tab
overlay_tab = self._create_overlay_tab()
tabs.addTab(overlay_tab, "Overlays")
# Data tab
data_tab = self._create_data_tab()
tabs.addTab(data_tab, "Data")
layout.addWidget(tabs)
# Save/Reset buttons
btn_layout = QHBoxLayout()
save_btn = QPushButton("Save Settings")
save_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
font-weight: bold;
}
""")
save_btn.clicked.connect(self._save_settings)
btn_layout.addWidget(save_btn)
reset_btn = QPushButton("Reset to Default")
reset_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,20);
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
}
""")
reset_btn.clicked.connect(self._reset_settings)
btn_layout.addWidget(reset_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
return widget
def _create_general_tab(self):
"""Create general settings tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Appearance
appear_group = QGroupBox("Appearance")
appear_group.setStyleSheet(self._group_style())
appear_layout = QVBoxLayout(appear_group)
# Theme
theme_layout = QHBoxLayout()
theme_layout.addWidget(QLabel("Theme:"))
self.theme_combo = QComboBox()
self.theme_combo.addItems(["Dark (EU Style)", "Light", "Auto"])
self.theme_combo.setCurrentText(self.settings.get('theme', 'Dark (EU Style)'))
theme_layout.addWidget(self.theme_combo)
theme_layout.addStretch()
appear_layout.addLayout(theme_layout)
# Opacity
opacity_layout = QHBoxLayout()
opacity_layout.addWidget(QLabel("Overlay Opacity:"))
self.opacity_slider = QSlider(Qt.Orientation.Horizontal)
self.opacity_slider.setMinimum(50)
self.opacity_slider.setMaximum(100)
self.opacity_slider.setValue(int(self.settings.get('overlay_opacity', 0.9) * 100))
opacity_layout.addWidget(self.opacity_slider)
self.opacity_label = QLabel(f"{self.opacity_slider.value()}%")
opacity_layout.addWidget(self.opacity_label)
opacity_layout.addStretch()
appear_layout.addLayout(opacity_layout)
# Icon size
icon_layout = QHBoxLayout()
icon_layout.addWidget(QLabel("Icon Size:"))
self.icon_combo = QComboBox()
self.icon_combo.addItems(["Small (20px)", "Medium (24px)", "Large (32px)"])
icon_layout.addWidget(self.icon_combo)
icon_layout.addStretch()
appear_layout.addLayout(icon_layout)
layout.addWidget(appear_group)
# Behavior
behavior_group = QGroupBox("Behavior")
behavior_group.setStyleSheet(self._group_style())
behavior_layout = QVBoxLayout(behavior_group)
self.auto_start_cb = QCheckBox("Start with Windows")
self.auto_start_cb.setChecked(self.settings.get('auto_start', False))
behavior_layout.addWidget(self.auto_start_cb)
self.minimize_cb = QCheckBox("Minimize to tray on close")
self.minimize_cb.setChecked(self.settings.get('minimize_to_tray', True))
behavior_layout.addWidget(self.minimize_cb)
self.tooltips_cb = QCheckBox("Show tooltips")
self.tooltips_cb.setChecked(self.settings.get('show_tooltips', True))
behavior_layout.addWidget(self.tooltips_cb)
layout.addWidget(behavior_group)
layout.addStretch()
return tab
def _create_plugins_tab(self):
"""Create plugins management tab with dependency visualization."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Info label
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)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
scroll_content = QWidget()
plugins_layout = QVBoxLayout(scroll_content)
plugins_layout.setSpacing(8)
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:
plugin_row = self._create_plugin_row(plugin_id, plugin_class, plugin_manager)
plugins_layout.addLayout(plugin_row)
# Separator
sep = QFrame()
sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet("background-color: rgba(100, 110, 130, 40);")
sep.setFixedHeight(1)
plugins_layout.addWidget(sep)
plugins_layout.addStretch()
scroll.setWidget(scroll_content)
layout.addWidget(scroll)
# Buttons
btn_layout = QHBoxLayout()
enable_all_btn = QPushButton("Enable All")
enable_all_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
""")
enable_all_btn.clicked.connect(self._enable_all_plugins)
btn_layout.addWidget(enable_all_btn)
disable_all_btn = QPushButton("Disable All")
disable_all_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,20);
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
""")
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 - dynamically discovers hotkeys from plugins."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# 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 = {}
# 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, config_key, default, description in core_hotkeys:
row = QHBoxLayout()
desc_label = QLabel(f"{label}:")
desc_label.setStyleSheet("color: white; min-width: 150px;")
row.addWidget(desc_label)
input_field = QLineEdit()
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);
color: white;
border: 1px solid rgba(100, 110, 130, 80);
padding: 5px;
min-width: 150px;
}
""")
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()
core_layout.addLayout(row)
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()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
overlays_group = QGroupBox("In-Game Overlays")
overlays_group.setStyleSheet(self._group_style())
overlays_layout = QVBoxLayout(overlays_group)
overlays = [
("Spotify Player", "spotify", True),
("Mission Tracker", "mission", False),
("Skill Gains", "skillgain", False),
("DPP Tracker", "dpp", False),
]
for name, key, enabled in overlays:
cb = QCheckBox(name)
cb.setChecked(enabled)
overlays_layout.addWidget(cb)
# Reset positions
reset_pos_btn = QPushButton("↺ Reset All Positions")
reset_pos_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,20);
color: white;
padding: 8px;
border: none;
border-radius: 4px;
}
""")
overlays_layout.addWidget(reset_pos_btn)
layout.addWidget(overlays_group)
layout.addStretch()
return tab
def _create_data_tab(self):
"""Create data management tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
data_group = QGroupBox("Data Management")
data_group.setStyleSheet(self._group_style())
data_layout = QVBoxLayout(data_group)
# Export
export_btn = QPushButton("📤 Export All Data")
export_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
color: white;
padding: 10px;
border: none;
border-radius: 4px;
font-weight: bold;
}
""")
export_btn.clicked.connect(self._export_data)
data_layout.addWidget(export_btn)
# Import
import_btn = QPushButton("📥 Import Data")
import_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,20);
color: white;
padding: 10px;
border: none;
border-radius: 4px;
}
""")
import_btn.clicked.connect(self._import_data)
data_layout.addWidget(import_btn)
# Clear
clear_btn = QPushButton("Clear All Data")
clear_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
padding: 10px;
border: none;
border-radius: 4px;
}
""")
clear_btn.clicked.connect(self._clear_data)
data_layout.addWidget(clear_btn)
# Retention
retention_layout = QHBoxLayout()
retention_layout.addWidget(QLabel("Data retention:"))
self.retention_combo = QComboBox()
self.retention_combo.addItems(["7 days", "30 days", "90 days", "Forever"])
retention_layout.addWidget(self.retention_combo)
retention_layout.addStretch()
data_layout.addLayout(retention_layout)
layout.addWidget(data_group)
layout.addStretch()
return tab
def _group_style(self):
"""Get group box style."""
return """
QGroupBox {
color: rgba(255,255,255,200);
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 6px;
margin-top: 10px;
font-weight: bold;
font-size: 12px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
}
"""
def _save_settings(self):
"""Save all settings."""
# General
self.settings.set('theme', self.theme_combo.currentText())
self.settings.set('overlay_opacity', self.opacity_slider.value() / 100)
self.settings.set('auto_start', self.auto_start_cb.isChecked())
self.settings.set('minimize_to_tray', self.minimize_cb.isChecked())
self.settings.set('show_tooltips', self.tooltips_cb.isChecked())
# 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!")
def _reset_settings(self):
"""Reset to defaults."""
self.settings.reset()
print("Settings reset to defaults!")
def _export_data(self):
"""Export all data."""
from PyQt6.QtWidgets import QFileDialog
filepath, _ = QFileDialog.getSaveFileName(
None, "Export EU-Utility Data", "eu_utility_backup.json", "JSON (*.json)"
)
if filepath:
import shutil
data_dir = Path("data")
if data_dir.exists():
# Create export
import json
export_data = {}
for f in data_dir.glob("*.json"):
with open(f, 'r') as file:
export_data[f.stem] = json.load(file)
with open(filepath, 'w') as file:
json.dump(export_data, file, indent=2)
def _import_data(self):
"""Import data."""
pass
def _clear_data(self):
"""Clear all data."""
pass
def _toggle_plugin(self, plugin_id: str, enable: bool):
"""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 = []
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 with dependency resolution."""
if not hasattr(self.overlay, 'plugin_manager'):
return
plugin_manager = self.overlay.plugin_manager
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 in reverse dependency order."""
if not hasattr(self.overlay, 'plugin_manager'):
return
plugin_manager = self.overlay.plugin_manager
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")