Files
Jackify/jackify/frontends/gui/screens/configure_tool_config_screen.py
2026-04-20 20:57:23 +01:00

443 lines
18 KiB
Python

"""
Configure Tool Compatibility screen.
Applies Wine registry settings for modding tools (xEdit, Pandora, DLL overrides)
to an existing configured modlist prefix. Available from Additional Tasks.
"""
import logging
import subprocess
from typing import Optional
from PySide6.QtCore import Qt, QSize, QThread, QTimer, Signal
from PySide6.QtWidgets import (
QCheckBox, QComboBox, QGridLayout, QHBoxLayout, QLabel,
QPushButton, QSizePolicy, QTabWidget, QTextEdit, QVBoxLayout, QWidget,
)
from jackify.frontends.gui.mixins.thread_lifecycle_mixin import ThreadLifecycleMixin
from jackify.frontends.gui.services.message_service import MessageService
from jackify.frontends.gui.shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
from jackify.frontends.gui.utils import set_responsive_minimum
logger = logging.getLogger(__name__)
class _ShortcutLoaderThread(QThread):
finished_signal = Signal(list) # list of {"name": str, "appid": str}
error_signal = Signal(str)
def run(self):
try:
from jackify.backend.handlers.modlist_handler import ModlistHandler
handler = ModlistHandler()
discovered = handler.discover_executable_shortcuts("ModOrganizer.exe")
shortcuts = [
{"name": m.get("name", "Unknown"), "appid": str(m.get("appid", ""))}
for m in discovered
if m.get("appid")
]
self.finished_signal.emit(shortcuts)
except Exception as e:
self.error_signal.emit(str(e))
class _ApplyThread(QThread):
log_signal = Signal(str)
finished_signal = Signal(bool)
def __init__(self, appid: str):
super().__init__()
self._appid = appid
def run(self):
from jackify.backend.services.tool_config_service import apply_tool_config_for_appid
ok = apply_tool_config_for_appid(self._appid, log=self.log_signal.emit)
self.finished_signal.emit(ok)
class ConfigureToolConfigScreen(ThreadLifecycleMixin, QWidget):
"""Apply tool compatibility settings to an existing modlist prefix."""
def __init__(self, stacked_widget=None, additional_tasks_index: int = 3, parent=None):
super().__init__(parent)
self.stacked_widget = stacked_widget
self.additional_tasks_index = additional_tasks_index
self.debug = DEBUG_BORDERS
self._shortcuts: list = []
self._loader: Optional[_ShortcutLoaderThread] = None
self._apply_thread: Optional[_ApplyThread] = None
self._setup_ui()
def _setup_ui(self):
main_vbox = QVBoxLayout(self)
main_vbox.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
main_vbox.setContentsMargins(50, 50, 50, 0)
main_vbox.setSpacing(12)
if self.debug:
self.setStyleSheet("border: 2px solid magenta;")
# --- Header ---
header_widget = QWidget()
header_layout = QVBoxLayout()
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(2)
title = QLabel("<b>Configure Tool Compatibility</b>")
title.setStyleSheet(f"font-size: 20px; color: {JACKIFY_COLOR_BLUE};")
title.setAlignment(Qt.AlignHCenter)
header_layout.addWidget(title)
header_layout.addSpacing(10)
desc = QLabel(
"Applies Wine registry settings needed for modding tools to work correctly: "
"xEdit family (WinXP compatibility), Pandora (window decoration), "
"and global DLL overrides."
)
desc.setWordWrap(True)
desc.setStyleSheet("color: #ccc; font-size: 13px;")
desc.setAlignment(Qt.AlignHCenter)
desc.setMaximumHeight(50)
header_layout.addWidget(desc)
header_layout.addSpacing(12)
header_widget.setLayout(header_layout)
header_widget.setFixedHeight(120)
if self.debug:
header_widget.setStyleSheet("border: 2px solid pink;")
main_vbox.addWidget(header_widget)
# --- Upper section: form (left) + tabs (right) ---
upper_hbox = QHBoxLayout()
upper_hbox.setContentsMargins(0, 0, 0, 0)
upper_hbox.setSpacing(16)
# Left: form
user_config_vbox = QVBoxLayout()
user_config_vbox.setAlignment(Qt.AlignTop)
user_config_vbox.setSpacing(4)
options_header = QLabel("<b>[Options]</b>")
options_header.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px; font-weight: bold;")
options_header.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
user_config_vbox.addWidget(options_header)
form_grid = QGridLayout()
form_grid.setHorizontalSpacing(12)
form_grid.setVerticalSpacing(6)
form_grid.setContentsMargins(0, 0, 0, 0)
modlist_label = QLabel("Modlist:")
form_grid.addWidget(modlist_label, 0, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
self._combo = QComboBox()
self._combo.setMinimumWidth(280)
self._combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self._combo.addItem("Loading modlists...")
self._combo.setEnabled(False)
form_grid.addWidget(self._combo, 0, 1)
form_widget = QWidget()
form_widget.setLayout(form_grid)
form_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
user_config_vbox.addWidget(form_widget)
user_config_vbox.addSpacing(10)
tools_info = QLabel(
"<b>Tools configured by this workflow:</b><br>"
"&nbsp;&nbsp;xEdit family &nbsp;|&nbsp; Synthesis &nbsp;|&nbsp; Pandora<br>"
"<br>"
"Run this once after installing a modlist if modding tools are not "
"launching correctly from within Mod Organizer 2.<br>"
"<br>"
"<i>These fixes are applied on a best-effort basis. Tool compatibility "
"can change with Proton and Wine updates. Not all tools are guaranteed "
"to work on all Proton versions.</i>"
)
tools_info.setWordWrap(True)
tools_info.setStyleSheet("color: #aaa; font-size: 12px;")
tools_info.setAlignment(Qt.AlignLeft | Qt.AlignTop)
user_config_vbox.addWidget(tools_info)
# Buttons (apply + back) - placed in left column like other screens
btn_row = QHBoxLayout()
btn_row.setAlignment(Qt.AlignHCenter)
self._apply_btn = QPushButton("Apply Tool Configurations")
self._apply_btn.setEnabled(False)
btn_row.addWidget(self._apply_btn)
self._back_btn = QPushButton("Back")
self._back_btn.clicked.connect(self._go_back)
btn_row.addWidget(self._back_btn)
btn_row.insertStretch(0, 1)
btn_row.addStretch(1)
self.show_details_checkbox = QCheckBox("Show details")
self.show_details_checkbox.setChecked(False)
self.show_details_checkbox.setToolTip("Toggle between activity summary and detailed console output")
self.show_details_checkbox.toggled.connect(self._on_show_details_toggled)
btn_row_widget = QWidget()
btn_row_widget.setLayout(btn_row)
btn_row_widget.setMaximumHeight(50)
if self.debug:
btn_row_widget.setStyleSheet("border: 2px solid red;")
self.btn_row_widget = btn_row_widget
user_config_widget = QWidget()
user_config_widget.setLayout(user_config_vbox)
user_config_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
if self.debug:
user_config_widget.setStyleSheet("border: 2px solid orange;")
# Right: Activity + Process Monitor tabs
self._activity_log = QTextEdit()
self._activity_log.setReadOnly(True)
self._activity_log.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
self._activity_log.setMinimumSize(QSize(300, 20))
self._activity_log.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._activity_log.setStyleSheet(
f"background: #222; color: {JACKIFY_COLOR_BLUE}; "
"font-family: monospace; font-size: 11px; border: 1px solid #444;"
)
self.process_monitor = QTextEdit()
self.process_monitor.setReadOnly(True)
self.process_monitor.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
self.process_monitor.setMinimumSize(QSize(300, 20))
self.process_monitor.setStyleSheet(
f"background: #222; color: {JACKIFY_COLOR_BLUE}; "
"font-family: monospace; font-size: 11px; border: 1px solid #444;"
)
process_vbox = QVBoxLayout()
process_vbox.setContentsMargins(0, 0, 0, 0)
process_vbox.setSpacing(2)
process_vbox.addWidget(self.process_monitor)
process_monitor_widget = QWidget()
process_monitor_widget.setLayout(process_vbox)
process_monitor_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
if self.debug:
process_monitor_widget.setStyleSheet("border: 2px solid purple;")
self.process_monitor_widget = process_monitor_widget
self.activity_tabs = QTabWidget()
self.activity_tabs.setStyleSheet(
"QTabWidget::pane { background: #222; border: 1px solid #444; } "
"QTabBar::tab { background: #222; color: #ccc; padding: 6px 16px; } "
"QTabBar::tab:selected { background: #333; color: #3fd0ea; } "
"QTabWidget { margin: 0px; padding: 0px; } "
"QTabBar { margin: 0px; padding: 0px; }"
)
self.activity_tabs.setContentsMargins(0, 0, 0, 0)
self.activity_tabs.setDocumentMode(False)
self.activity_tabs.setTabPosition(QTabWidget.North)
if self.debug:
self.activity_tabs.setStyleSheet("border: 2px solid cyan;")
self.activity_tabs.addTab(self._activity_log, "Activity")
self.activity_tabs.addTab(process_monitor_widget, "Process Monitor")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(self.activity_tabs, stretch=9)
upper_hbox.setAlignment(Qt.AlignTop)
upper_section_widget = QWidget()
upper_section_widget.setLayout(upper_hbox)
upper_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
upper_section_widget.setMaximumHeight(280)
if self.debug:
upper_section_widget.setStyleSheet("border: 2px solid green;")
main_vbox.addWidget(upper_section_widget)
# --- Status banner ---
self._status_banner = QLabel("Ready to apply")
self._status_banner.setAlignment(Qt.AlignCenter)
self._status_banner.setStyleSheet(f"""
background-color: #2a2a2a;
color: {JACKIFY_COLOR_BLUE};
padding: 6px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 13px;
""")
self._status_banner.setMaximumHeight(34)
self._status_banner.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
banner_row = QHBoxLayout()
banner_row.setContentsMargins(0, 0, 0, 0)
banner_row.setSpacing(8)
banner_row.addWidget(self._status_banner, 1)
banner_row.addStretch()
banner_row.addWidget(self.show_details_checkbox)
banner_row_widget = QWidget()
banner_row_widget.setLayout(banner_row)
banner_row_widget.setMaximumHeight(45)
banner_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
main_vbox.addWidget(banner_row_widget)
# --- Console (hidden by default) ---
self.console = QTextEdit()
self.console.setReadOnly(True)
self.console.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
self.console.setMinimumHeight(0)
self.console.setMaximumHeight(0)
self.console.setFontFamily("monospace")
if self.debug:
self.console.setStyleSheet("border: 2px solid yellow;")
main_vbox.addWidget(self.console, stretch=1)
main_vbox.addWidget(btn_row_widget, alignment=Qt.AlignHCenter)
self.main_overall_vbox = main_vbox
self.setLayout(main_vbox)
# Process monitor refresh timer
self._top_timer = QTimer(self)
self._top_timer.timeout.connect(self._update_top_panel)
self._top_timer.start(2000)
self._apply_btn.clicked.connect(self._on_apply)
# ------------------------------------------------------------------
def showEvent(self, event):
super().showEvent(event)
logger.info("Configure Tool Compatibility screen opened")
try:
main_window = self.window()
if main_window:
set_responsive_minimum(main_window, min_width=960, min_height=520)
except Exception:
pass
self._load_shortcuts()
def _load_shortcuts(self):
if self._loader and self._loader.isRunning():
return
self._combo.clear()
self._combo.addItem("Loading modlists...")
self._combo.setEnabled(False)
self._apply_btn.setEnabled(False)
self._loader = _ShortcutLoaderThread()
self._loader.finished_signal.connect(self._on_shortcuts_loaded)
self._loader.error_signal.connect(self._on_shortcuts_error)
self._loader.start()
def _on_shortcuts_loaded(self, shortcuts: list):
self._shortcuts = shortcuts
self._combo.clear()
if not shortcuts:
self._combo.addItem("No configured modlists found")
self._combo.setEnabled(False)
self._apply_btn.setEnabled(False)
return
for s in shortcuts:
self._combo.addItem(s["name"])
self._combo.setEnabled(True)
self._apply_btn.setEnabled(True)
def _on_shortcuts_error(self, error: str):
self._combo.clear()
self._combo.addItem("Error loading modlists")
self._combo.setEnabled(False)
self._activity_log.append(f"Failed to load modlists: {error}")
def _on_apply(self):
idx = self._combo.currentIndex()
if idx < 0 or idx >= len(self._shortcuts):
return
shortcut = self._shortcuts[idx]
appid = shortcut["appid"]
name = shortcut["name"]
self._activity_log.clear()
self.console.clear()
self._activity_log.append(f"Applying tool configurations to: {name} (AppID {appid})")
self._status_banner.setText("Applying...")
logger.info("Applying tool compat config: %s (AppID %s)", name, appid)
self._apply_btn.setEnabled(False)
self._combo.setEnabled(False)
self._apply_thread = _ApplyThread(appid)
self._apply_thread.log_signal.connect(self._activity_log.append)
self._apply_thread.log_signal.connect(self.console.append)
self._apply_thread.finished_signal.connect(self._on_apply_finished)
self._apply_thread.start()
def _on_apply_finished(self, success: bool):
self._apply_thread = None
self._apply_btn.setEnabled(True)
self._combo.setEnabled(True)
if success:
self._activity_log.append("\nDone. Tool compatibility settings applied successfully.")
self._status_banner.setText("Applied successfully")
logger.info("Tool compat config applied successfully")
idx = self._combo.currentIndex()
name = self._shortcuts[idx]["name"] if 0 <= idx < len(self._shortcuts) else "Modlist"
from ..dialogs.success_dialog import SuccessDialog
success_dialog = SuccessDialog(
modlist_name=name,
workflow_type="tool_config",
time_taken="",
parent=self
)
success_dialog.show()
else:
self._activity_log.append("\nFailed. Check the output above for details.")
self._status_banner.setText("Apply failed - see details")
logger.warning("Tool compat config failed")
MessageService.warning(
self, "Failed",
"Tool configuration did not complete successfully.\nSee the log for details."
)
def _go_back(self):
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(self.additional_tasks_index)
def _on_show_details_toggled(self, checked: bool):
if checked:
self.console.setMinimumHeight(200)
self.console.setMaximumHeight(16777215)
self.console.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
else:
self.console.setMinimumHeight(0)
self.console.setMaximumHeight(0)
self.console.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
def _update_top_panel(self):
try:
result = subprocess.run(
["ps", "-eo", "pcpu,pmem,comm,args"],
stdout=subprocess.PIPE, text=True, timeout=2
)
lines = result.stdout.splitlines()
header = "CPU%\tMEM%\tCOMMAND"
filtered = [header]
rows = []
for line in lines[1:]:
ll = line.lower()
if (
"wine" in ll or "wine64" in ll or "protontricks" in ll
or "jackify-engine" in ll
) and "jackify-gui.py" not in ll:
cols = line.strip().split(None, 3)
if len(cols) >= 3:
rows.append(cols)
rows.sort(key=lambda x: float(x[0]), reverse=True)
for cols in rows:
filtered.append("\t".join(cols))
if len(filtered) == 1:
filtered.append("[No relevant processes]")
self.process_monitor.setPlainText("\n".join(filtered))
except Exception as e:
self.process_monitor.setPlainText(f"[process info unavailable: {e}]")
def cleanup_processes(self):
self._park_all_threads()