1175 lines
43 KiB
Python
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")
|