mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Sync from development - prepare for v0.1.2
This commit is contained in:
400
jackify/frontends/gui/dialogs/about_dialog.py
Normal file
400
jackify/frontends/gui/dialogs/about_dialog.py
Normal file
@@ -0,0 +1,400 @@
|
||||
"""
|
||||
About dialog for Jackify.
|
||||
|
||||
This dialog displays system information, version details, and provides
|
||||
access to update checking and external links.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QGroupBox, QTextEdit, QApplication
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont, QClipboard
|
||||
|
||||
from ....backend.services.update_service import UpdateService
|
||||
from ....backend.models.configuration import SystemInfo
|
||||
from .... import __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateCheckThread(QThread):
|
||||
"""Background thread for checking updates."""
|
||||
|
||||
update_check_finished = Signal(object) # UpdateInfo or None
|
||||
|
||||
def __init__(self, update_service: UpdateService):
|
||||
super().__init__()
|
||||
self.update_service = update_service
|
||||
|
||||
def run(self):
|
||||
"""Check for updates in background."""
|
||||
try:
|
||||
update_info = self.update_service.check_for_updates()
|
||||
self.update_check_finished.emit(update_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking for updates: {e}")
|
||||
self.update_check_finished.emit(None)
|
||||
|
||||
|
||||
class AboutDialog(QDialog):
|
||||
"""About dialog showing system info and app details."""
|
||||
|
||||
def __init__(self, system_info: SystemInfo, parent=None):
|
||||
super().__init__(parent)
|
||||
self.system_info = system_info
|
||||
self.update_service = UpdateService(__version__)
|
||||
self.update_check_thread = None
|
||||
|
||||
self.setup_ui()
|
||||
self.setup_connections()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Set up the dialog UI."""
|
||||
self.setWindowTitle("About Jackify")
|
||||
self.setModal(True)
|
||||
self.setFixedSize(520, 520)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Header
|
||||
header_layout = QVBoxLayout()
|
||||
|
||||
# App icon/name
|
||||
title_label = QLabel("Jackify")
|
||||
title_font = QFont()
|
||||
title_font.setPointSize(18)
|
||||
title_font.setBold(True)
|
||||
title_label.setFont(title_font)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet("color: #3fd0ea; margin: 10px;")
|
||||
header_layout.addWidget(title_label)
|
||||
|
||||
subtitle_label = QLabel(f"v{__version__}")
|
||||
subtitle_font = QFont()
|
||||
subtitle_font.setPointSize(12)
|
||||
subtitle_label.setFont(subtitle_font)
|
||||
subtitle_label.setAlignment(Qt.AlignCenter)
|
||||
subtitle_label.setStyleSheet("color: #666; margin-bottom: 10px;")
|
||||
header_layout.addWidget(subtitle_label)
|
||||
|
||||
tagline_label = QLabel("Simplifying Wabbajack modlist installation and configuration on Linux")
|
||||
tagline_label.setAlignment(Qt.AlignCenter)
|
||||
tagline_label.setStyleSheet("color: #888; margin-bottom: 20px;")
|
||||
header_layout.addWidget(tagline_label)
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# System Information Group
|
||||
system_group = QGroupBox("System Information")
|
||||
system_layout = QVBoxLayout(system_group)
|
||||
|
||||
system_info_text = self._get_system_info_text()
|
||||
system_info_label = QLabel(system_info_text)
|
||||
system_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
|
||||
system_info_label.setWordWrap(True)
|
||||
system_layout.addWidget(system_info_label)
|
||||
|
||||
layout.addWidget(system_group)
|
||||
|
||||
# Jackify Information Group
|
||||
jackify_group = QGroupBox("Jackify Information")
|
||||
jackify_layout = QVBoxLayout(jackify_group)
|
||||
|
||||
jackify_info_text = self._get_jackify_info_text()
|
||||
jackify_info_label = QLabel(jackify_info_text)
|
||||
jackify_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
|
||||
jackify_layout.addWidget(jackify_info_label)
|
||||
|
||||
layout.addWidget(jackify_group)
|
||||
|
||||
# Update status
|
||||
self.update_status_label = QLabel("")
|
||||
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
|
||||
self.update_status_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(self.update_status_label)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
# Update check button
|
||||
self.update_button = QPushButton("Check for Updates")
|
||||
self.update_button.clicked.connect(self.check_for_updates)
|
||||
self.update_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #23272e;
|
||||
color: #3fd0ea;
|
||||
font-weight: bold;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #3fd0ea;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #3fd0ea;
|
||||
color: #23272e;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2bb8d6;
|
||||
color: #23272e;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #444;
|
||||
color: #666;
|
||||
border-color: #666;
|
||||
}
|
||||
""")
|
||||
button_layout.addWidget(self.update_button)
|
||||
|
||||
button_layout.addStretch()
|
||||
|
||||
# Copy Info button
|
||||
copy_button = QPushButton("Copy Info")
|
||||
copy_button.clicked.connect(self.copy_system_info)
|
||||
button_layout.addWidget(copy_button)
|
||||
|
||||
# External links
|
||||
github_button = QPushButton("GitHub")
|
||||
github_button.clicked.connect(self.open_github)
|
||||
button_layout.addWidget(github_button)
|
||||
|
||||
nexus_button = QPushButton("Nexus")
|
||||
nexus_button.clicked.connect(self.open_nexus)
|
||||
button_layout.addWidget(nexus_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Close button
|
||||
close_layout = QHBoxLayout()
|
||||
close_layout.addStretch()
|
||||
close_button = QPushButton("Close")
|
||||
close_button.setDefault(True)
|
||||
close_button.clicked.connect(self.accept)
|
||||
close_layout.addWidget(close_button)
|
||||
layout.addLayout(close_layout)
|
||||
|
||||
def setup_connections(self):
|
||||
"""Set up signal connections."""
|
||||
pass
|
||||
|
||||
def _get_system_info_text(self) -> str:
|
||||
"""Get formatted system information."""
|
||||
try:
|
||||
# OS info
|
||||
os_info = self._get_os_info()
|
||||
kernel = platform.release()
|
||||
|
||||
# Desktop environment
|
||||
desktop = self._get_desktop_environment()
|
||||
|
||||
# Display server
|
||||
display_server = self._get_display_server()
|
||||
|
||||
return f"• OS: {os_info}\n• Kernel: {kernel}\n• Desktop: {desktop}\n• Display: {display_server}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting system info: {e}")
|
||||
return "• System info unavailable"
|
||||
|
||||
def _get_jackify_info_text(self) -> str:
|
||||
"""Get formatted Jackify information."""
|
||||
try:
|
||||
# Engine version
|
||||
engine_version = self._get_engine_version()
|
||||
|
||||
# Python version
|
||||
python_version = platform.python_version()
|
||||
|
||||
return f"• Engine: {engine_version}\n• Python: {python_version}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Jackify info: {e}")
|
||||
return "• Jackify info unavailable"
|
||||
|
||||
def _get_os_info(self) -> str:
|
||||
"""Get OS distribution name and version."""
|
||||
try:
|
||||
if os.path.exists("/etc/os-release"):
|
||||
with open("/etc/os-release", "r") as f:
|
||||
lines = f.readlines()
|
||||
pretty_name = None
|
||||
name = None
|
||||
version = None
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("PRETTY_NAME="):
|
||||
pretty_name = line.split("=", 1)[1].strip('"')
|
||||
elif line.startswith("NAME="):
|
||||
name = line.split("=", 1)[1].strip('"')
|
||||
elif line.startswith("VERSION="):
|
||||
version = line.split("=", 1)[1].strip('"')
|
||||
|
||||
# Prefer PRETTY_NAME, fallback to NAME + VERSION
|
||||
if pretty_name:
|
||||
return pretty_name
|
||||
elif name and version:
|
||||
return f"{name} {version}"
|
||||
elif name:
|
||||
return name
|
||||
|
||||
# Fallback to platform info
|
||||
return f"{platform.system()} {platform.release()}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting OS info: {e}")
|
||||
return "Unknown Linux"
|
||||
|
||||
def _get_desktop_environment(self) -> str:
|
||||
"""Get desktop environment."""
|
||||
try:
|
||||
# Try XDG_CURRENT_DESKTOP first
|
||||
desktop = os.environ.get("XDG_CURRENT_DESKTOP")
|
||||
if desktop:
|
||||
return desktop
|
||||
|
||||
# Fallback to DESKTOP_SESSION
|
||||
desktop = os.environ.get("DESKTOP_SESSION")
|
||||
if desktop:
|
||||
return desktop
|
||||
|
||||
# Try detecting common DEs
|
||||
if os.environ.get("KDE_FULL_SESSION"):
|
||||
return "KDE"
|
||||
elif os.environ.get("GNOME_DESKTOP_SESSION_ID"):
|
||||
return "GNOME"
|
||||
elif os.environ.get("XFCE4_SESSION"):
|
||||
return "XFCE"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting desktop environment: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def _get_display_server(self) -> str:
|
||||
"""Get display server type (Wayland or X11)."""
|
||||
try:
|
||||
# Check XDG_SESSION_TYPE first
|
||||
session_type = os.environ.get("XDG_SESSION_TYPE")
|
||||
if session_type:
|
||||
return session_type.capitalize()
|
||||
|
||||
# Check for Wayland display
|
||||
if os.environ.get("WAYLAND_DISPLAY"):
|
||||
return "Wayland"
|
||||
|
||||
# Check for X11 display
|
||||
if os.environ.get("DISPLAY"):
|
||||
return "X11"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting display server: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def _get_engine_version(self) -> str:
|
||||
"""Get jackify-engine version."""
|
||||
try:
|
||||
# Try to execute jackify-engine --version
|
||||
engine_path = Path(__file__).parent.parent.parent.parent / "engine" / "jackify-engine"
|
||||
if engine_path.exists():
|
||||
result = subprocess.run([str(engine_path), "--version"],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
version = result.stdout.strip()
|
||||
# Extract just the version number (before the +commit hash)
|
||||
if '+' in version:
|
||||
version = version.split('+')[0]
|
||||
return f"v{version}"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting engine version: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def check_for_updates(self):
|
||||
"""Check for updates in background."""
|
||||
if self.update_check_thread and self.update_check_thread.isRunning():
|
||||
return
|
||||
|
||||
self.update_button.setEnabled(False)
|
||||
self.update_button.setText("Checking...")
|
||||
self.update_status_label.setText("Checking for updates...")
|
||||
|
||||
self.update_check_thread = UpdateCheckThread(self.update_service)
|
||||
self.update_check_thread.update_check_finished.connect(self.update_check_finished)
|
||||
self.update_check_thread.start()
|
||||
|
||||
def update_check_finished(self, update_info):
|
||||
"""Handle update check completion."""
|
||||
self.update_button.setEnabled(True)
|
||||
self.update_button.setText("Check for Updates")
|
||||
|
||||
if update_info:
|
||||
self.update_status_label.setText(f"Update available: v{update_info.version}")
|
||||
self.update_status_label.setStyleSheet("color: #3fd0ea; font-size: 10pt; margin: 5px;")
|
||||
|
||||
# Show update dialog
|
||||
from .update_dialog import UpdateDialog
|
||||
update_dialog = UpdateDialog(update_info, self.update_service, self)
|
||||
update_dialog.exec()
|
||||
else:
|
||||
self.update_status_label.setText("You're running the latest version")
|
||||
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
|
||||
|
||||
def copy_system_info(self):
|
||||
"""Copy system information to clipboard."""
|
||||
try:
|
||||
info_text = f"""Jackify v{__version__} (Engine {self._get_engine_version()})
|
||||
OS: {self._get_os_info()} ({platform.release()})
|
||||
Desktop: {self._get_desktop_environment()} ({self._get_display_server()})
|
||||
Python: {platform.python_version()}"""
|
||||
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(info_text)
|
||||
|
||||
# Briefly update button text
|
||||
sender = self.sender()
|
||||
original_text = sender.text()
|
||||
sender.setText("Copied!")
|
||||
|
||||
# Reset button text after delay
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(1000, lambda: sender.setText(original_text))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error copying system info: {e}")
|
||||
|
||||
def open_github(self):
|
||||
"""Open GitHub repository."""
|
||||
try:
|
||||
import webbrowser
|
||||
webbrowser.open("https://github.com/Omni-guides/Jackify")
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening GitHub: {e}")
|
||||
|
||||
def open_nexus(self):
|
||||
"""Open Nexus Mods page."""
|
||||
try:
|
||||
import webbrowser
|
||||
webbrowser.open("https://www.nexusmods.com/site/mods/1427")
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening Nexus: {e}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle dialog close event."""
|
||||
if self.update_check_thread and self.update_check_thread.isRunning():
|
||||
self.update_check_thread.terminate()
|
||||
self.update_check_thread.wait()
|
||||
|
||||
event.accept()
|
||||
@@ -325,6 +325,29 @@ class SettingsDialog(QDialog):
|
||||
download_dir_row.addWidget(self.download_dir_edit)
|
||||
download_dir_row.addWidget(self.download_dir_btn)
|
||||
dir_layout.addRow(QLabel("Downloads Base Dir:"), download_dir_row)
|
||||
|
||||
# Jackify Data Directory
|
||||
from jackify.shared.paths import get_jackify_data_dir
|
||||
current_jackify_dir = str(get_jackify_data_dir())
|
||||
self.jackify_data_dir_edit = QLineEdit(current_jackify_dir)
|
||||
self.jackify_data_dir_edit.setToolTip("Directory for Jackify data (logs, downloads, temp files). Default: ~/Jackify")
|
||||
self.jackify_data_dir_btn = QPushButton()
|
||||
self.jackify_data_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
|
||||
self.jackify_data_dir_btn.setToolTip("Browse for directory")
|
||||
self.jackify_data_dir_btn.setFixedWidth(32)
|
||||
self.jackify_data_dir_btn.clicked.connect(lambda: self._pick_directory(self.jackify_data_dir_edit))
|
||||
jackify_data_dir_row = QHBoxLayout()
|
||||
jackify_data_dir_row.addWidget(self.jackify_data_dir_edit)
|
||||
jackify_data_dir_row.addWidget(self.jackify_data_dir_btn)
|
||||
|
||||
# Reset to default button
|
||||
reset_jackify_dir_btn = QPushButton("Reset")
|
||||
reset_jackify_dir_btn.setToolTip("Reset to default (~/ Jackify)")
|
||||
reset_jackify_dir_btn.setFixedWidth(50)
|
||||
reset_jackify_dir_btn.clicked.connect(lambda: self.jackify_data_dir_edit.setText(str(Path.home() / "Jackify")))
|
||||
jackify_data_dir_row.addWidget(reset_jackify_dir_btn)
|
||||
|
||||
dir_layout.addRow(QLabel("Jackify Data Dir:"), jackify_data_dir_row)
|
||||
main_layout.addWidget(dir_group)
|
||||
main_layout.addSpacing(12)
|
||||
|
||||
@@ -464,7 +487,14 @@ class SettingsDialog(QDialog):
|
||||
# Save modlist base dirs
|
||||
self.config_handler.set("modlist_install_base_dir", self.install_dir_edit.text().strip())
|
||||
self.config_handler.set("modlist_downloads_base_dir", self.download_dir_edit.text().strip())
|
||||
# Save jackify data directory (always store actual path, never None)
|
||||
jackify_data_dir = self.jackify_data_dir_edit.text().strip()
|
||||
self.config_handler.set("jackify_data_dir", jackify_data_dir)
|
||||
self.config_handler.save_config()
|
||||
|
||||
# Refresh cached paths in GUI screens if Jackify directory changed
|
||||
self._refresh_gui_paths()
|
||||
|
||||
# Check if debug mode changed and prompt for restart
|
||||
new_debug_mode = self.debug_checkbox.isChecked()
|
||||
if new_debug_mode != self._original_debug_mode:
|
||||
@@ -484,6 +514,29 @@ class SettingsDialog(QDialog):
|
||||
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
|
||||
self.accept()
|
||||
|
||||
def _refresh_gui_paths(self):
|
||||
"""Refresh cached paths in all GUI screens."""
|
||||
try:
|
||||
# Get the main window through parent relationship
|
||||
main_window = self.parent()
|
||||
if not main_window or not hasattr(main_window, 'stacked_widget'):
|
||||
return
|
||||
|
||||
# Refresh paths in all screens that have the method
|
||||
screens_to_refresh = [
|
||||
getattr(main_window, 'install_modlist_screen', None),
|
||||
getattr(main_window, 'configure_new_modlist_screen', None),
|
||||
getattr(main_window, 'configure_existing_modlist_screen', None),
|
||||
getattr(main_window, 'tuxborn_screen', None),
|
||||
]
|
||||
|
||||
for screen in screens_to_refresh:
|
||||
if screen and hasattr(screen, 'refresh_paths'):
|
||||
screen.refresh_paths()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not refresh GUI paths: {e}")
|
||||
|
||||
def _bold_label(self, text):
|
||||
label = QLabel(text)
|
||||
label.setStyleSheet("font-weight: bold; color: #fff;")
|
||||
@@ -655,7 +708,7 @@ class JackifyMainWindow(QMainWindow):
|
||||
# Spacer
|
||||
bottom_bar_layout.addStretch(1)
|
||||
|
||||
# Settings button (right)
|
||||
# Settings button (right side)
|
||||
settings_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">Settings</a>')
|
||||
settings_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
|
||||
settings_btn.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
@@ -663,6 +716,14 @@ class JackifyMainWindow(QMainWindow):
|
||||
settings_btn.linkActivated.connect(self.open_settings_dialog)
|
||||
bottom_bar_layout.addWidget(settings_btn, alignment=Qt.AlignRight)
|
||||
|
||||
# About button (right side)
|
||||
about_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">About</a>')
|
||||
about_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
|
||||
about_btn.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
about_btn.setOpenExternalLinks(False)
|
||||
about_btn.linkActivated.connect(self.open_about_dialog)
|
||||
bottom_bar_layout.addWidget(about_btn, alignment=Qt.AlignRight)
|
||||
|
||||
# --- Main Layout ---
|
||||
central_widget = QWidget()
|
||||
main_layout = QVBoxLayout()
|
||||
@@ -808,6 +869,16 @@ class JackifyMainWindow(QMainWindow):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def open_about_dialog(self):
|
||||
try:
|
||||
from jackify.frontends.gui.dialogs.about_dialog import AboutDialog
|
||||
dlg = AboutDialog(self.system_info, self)
|
||||
dlg.exec()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception in open_about_dialog: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def resource_path(relative_path):
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
|
||||
@@ -34,8 +34,7 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
self.stacked_widget = stacked_widget
|
||||
self.main_menu_index = main_menu_index
|
||||
self.debug = DEBUG_BORDERS
|
||||
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_Existing_Modlist_workflow.log')
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
self.refresh_paths()
|
||||
|
||||
# --- Detect Steam Deck ---
|
||||
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
|
||||
@@ -297,6 +296,41 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
|
||||
# Time tracking for workflow completion
|
||||
self._workflow_start_time = None
|
||||
|
||||
# Initialize empty controls list - will be populated after UI is built
|
||||
self._actionable_controls = []
|
||||
|
||||
# Now collect all actionable controls after UI is fully built
|
||||
self._collect_actionable_controls()
|
||||
|
||||
def _collect_actionable_controls(self):
|
||||
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
|
||||
self._actionable_controls = [
|
||||
# Main action button
|
||||
self.start_btn,
|
||||
# Form fields
|
||||
self.shortcut_combo,
|
||||
# Resolution controls
|
||||
self.resolution_combo,
|
||||
]
|
||||
|
||||
def _disable_controls_during_operation(self):
|
||||
"""Disable all actionable controls during configure operations (except Cancel)"""
|
||||
for control in self._actionable_controls:
|
||||
if control:
|
||||
control.setEnabled(False)
|
||||
|
||||
def _enable_controls_after_operation(self):
|
||||
"""Re-enable all actionable controls after configure operations complete"""
|
||||
for control in self._actionable_controls:
|
||||
if control:
|
||||
control.setEnabled(True)
|
||||
|
||||
def refresh_paths(self):
|
||||
"""Refresh cached paths when config changes."""
|
||||
from jackify.shared.paths import get_jackify_logs_dir
|
||||
self.modlist_log_path = get_jackify_logs_dir() / 'Configure_Existing_Modlist_workflow.log'
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""Handle window resize to prioritize form over console"""
|
||||
@@ -382,17 +416,22 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
log_handler = LoggingHandler()
|
||||
log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5)
|
||||
|
||||
# Disable controls during configuration
|
||||
self._disable_controls_during_operation()
|
||||
|
||||
# Get selected shortcut
|
||||
idx = self.shortcut_combo.currentIndex() - 1 # Account for 'Please Select...'
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
if idx < 0 or idx >= len(self.shortcut_map):
|
||||
MessageService.critical(self, "No Shortcut Selected", "Please select a ModOrganizer.exe Steam shortcut to configure.", safety_level="medium")
|
||||
self._enable_controls_after_operation()
|
||||
return
|
||||
shortcut = self.shortcut_map[idx]
|
||||
modlist_name = shortcut.get('AppName', '')
|
||||
install_dir = shortcut.get('StartDir', '')
|
||||
if not modlist_name or not install_dir:
|
||||
MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium")
|
||||
self._enable_controls_after_operation()
|
||||
return
|
||||
resolution = self.resolution_combo.currentText()
|
||||
# Handle resolution saving
|
||||
@@ -505,6 +544,9 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
|
||||
def on_configuration_complete(self, success, message, modlist_name):
|
||||
"""Handle configuration completion"""
|
||||
# Re-enable all controls when workflow completes
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
if success:
|
||||
# Calculate time taken
|
||||
time_taken = self._calculate_time_taken()
|
||||
@@ -525,6 +567,9 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
|
||||
def on_configuration_error(self, error_message):
|
||||
"""Handle configuration error"""
|
||||
# Re-enable all controls on error
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
self._safe_append_text(f"Configuration error: {error_message}")
|
||||
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium")
|
||||
|
||||
@@ -559,8 +604,8 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
if self.config_process and self.config_process.state() == QProcess.Running:
|
||||
self.config_process.terminate()
|
||||
self.config_process.waitForFinished(2000)
|
||||
# Reset button states
|
||||
self.start_btn.setEnabled(True)
|
||||
# Re-enable all controls
|
||||
self._enable_controls_after_operation()
|
||||
self.cancel_btn.setVisible(True)
|
||||
|
||||
def show_next_steps_dialog(self, message):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
ConfigureNewModlistScreen for Jackify GUI
|
||||
"""
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QCheckBox
|
||||
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QProcess, QMetaObject
|
||||
from PySide6.QtGui import QPixmap, QTextCursor
|
||||
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
|
||||
@@ -106,8 +106,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
self.protontricks_service = ProtontricksDetectionService()
|
||||
|
||||
# Path for workflow log
|
||||
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_New_Modlist_workflow.log')
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
self.refresh_paths()
|
||||
|
||||
# Scroll tracking for professional auto-scroll behavior
|
||||
self._user_manually_scrolled = False
|
||||
@@ -211,7 +210,6 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
"7680x4320"
|
||||
])
|
||||
form_grid.addWidget(resolution_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
|
||||
form_grid.addWidget(self.resolution_combo, 2, 1)
|
||||
|
||||
# Load saved resolution if available
|
||||
saved_resolution = self.resolution_service.get_saved_resolution()
|
||||
@@ -236,6 +234,27 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
else:
|
||||
self.resolution_combo.setCurrentIndex(0)
|
||||
# Otherwise, default is 'Leave unchanged' (index 0)
|
||||
|
||||
# Horizontal layout for resolution dropdown and auto-restart checkbox
|
||||
resolution_and_restart_layout = QHBoxLayout()
|
||||
resolution_and_restart_layout.setSpacing(12)
|
||||
|
||||
# Resolution dropdown (made smaller)
|
||||
self.resolution_combo.setMaximumWidth(280) # Constrain width but keep aesthetically pleasing
|
||||
resolution_and_restart_layout.addWidget(self.resolution_combo)
|
||||
|
||||
# Add stretch to push checkbox to the right
|
||||
resolution_and_restart_layout.addStretch()
|
||||
|
||||
# Auto-accept Steam restart checkbox (right-aligned)
|
||||
self.auto_restart_checkbox = QCheckBox("Auto-accept Steam restart")
|
||||
self.auto_restart_checkbox.setChecked(False) # Always default to unchecked per session
|
||||
self.auto_restart_checkbox.setToolTip("When checked, Steam restart dialog will be automatically accepted, allowing unattended configuration")
|
||||
resolution_and_restart_layout.addWidget(self.auto_restart_checkbox)
|
||||
|
||||
# Update the form grid to use the combined layout
|
||||
form_grid.addLayout(resolution_and_restart_layout, 2, 1)
|
||||
|
||||
form_section_widget = QWidget()
|
||||
form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
form_section_widget.setLayout(form_grid)
|
||||
@@ -338,6 +357,44 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
self.start_btn.clicked.connect(self.validate_and_start_configure)
|
||||
# --- Connect steam_restart_finished signal ---
|
||||
self.steam_restart_finished.connect(self._on_steam_restart_finished)
|
||||
|
||||
# Initialize empty controls list - will be populated after UI is built
|
||||
self._actionable_controls = []
|
||||
|
||||
# Now collect all actionable controls after UI is fully built
|
||||
self._collect_actionable_controls()
|
||||
|
||||
def _collect_actionable_controls(self):
|
||||
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
|
||||
self._actionable_controls = [
|
||||
# Main action button
|
||||
self.start_btn,
|
||||
# Form fields
|
||||
self.modlist_name_edit,
|
||||
self.install_dir_edit,
|
||||
# Resolution controls
|
||||
self.resolution_combo,
|
||||
# Checkboxes
|
||||
self.auto_restart_checkbox,
|
||||
]
|
||||
|
||||
def _disable_controls_during_operation(self):
|
||||
"""Disable all actionable controls during configure operations (except Cancel)"""
|
||||
for control in self._actionable_controls:
|
||||
if control:
|
||||
control.setEnabled(False)
|
||||
|
||||
def _enable_controls_after_operation(self):
|
||||
"""Re-enable all actionable controls after configure operations complete"""
|
||||
for control in self._actionable_controls:
|
||||
if control:
|
||||
control.setEnabled(True)
|
||||
|
||||
def refresh_paths(self):
|
||||
"""Refresh cached paths when config changes."""
|
||||
from jackify.shared.paths import get_jackify_logs_dir
|
||||
self.modlist_log_path = get_jackify_logs_dir() / 'Configure_New_Modlist_workflow.log'
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""Handle window resize to prioritize form over console"""
|
||||
@@ -522,23 +579,38 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
# Start time tracking
|
||||
self._workflow_start_time = time.time()
|
||||
|
||||
# Disable controls during configuration (after validation passes)
|
||||
self._disable_controls_during_operation()
|
||||
|
||||
# Validate modlist name
|
||||
modlist_name = self.modlist_name_edit.text().strip()
|
||||
if not modlist_name:
|
||||
MessageService.warning(self, "Missing Name", "Please specify a name for your modlist", safety_level="low")
|
||||
self._enable_controls_after_operation()
|
||||
return
|
||||
# --- Shortcut creation will be handled by automated workflow ---
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
|
||||
shortcut_handler = ShortcutHandler(steamdeck=steamdeck) # Still needed for Steam restart
|
||||
# --- User confirmation before restarting Steam ---
|
||||
reply = MessageService.question(
|
||||
self, "Ready to Configure Modlist",
|
||||
"Would you like to restart Steam and begin post-install configuration now? Restarting Steam could close any games you have open!",
|
||||
safety_level="medium"
|
||||
)
|
||||
print(f"DEBUG: Steam restart dialog returned: {reply!r}")
|
||||
|
||||
# Check if auto-restart is enabled
|
||||
auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked()
|
||||
|
||||
if auto_restart_enabled:
|
||||
# Auto-accept Steam restart - proceed without dialog
|
||||
self._safe_append_text("Auto-accepting Steam restart (unattended mode enabled)")
|
||||
reply = QMessageBox.Yes # Simulate user clicking Yes
|
||||
else:
|
||||
# --- User confirmation before restarting Steam ---
|
||||
reply = MessageService.question(
|
||||
self, "Ready to Configure Modlist",
|
||||
"Would you like to restart Steam and begin post-install configuration now? Restarting Steam could close any games you have open!",
|
||||
safety_level="medium"
|
||||
)
|
||||
|
||||
debug_print(f"DEBUG: Steam restart dialog returned: {reply!r}")
|
||||
if reply not in (QMessageBox.Yes, QMessageBox.Ok, QMessageBox.AcceptRole):
|
||||
self._enable_controls_after_operation()
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
return
|
||||
@@ -562,7 +634,6 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
progress.setMinimumDuration(0)
|
||||
progress.setValue(0)
|
||||
progress.show()
|
||||
self.setEnabled(False)
|
||||
def do_restart():
|
||||
try:
|
||||
ok = shortcut_handler.secure_steam_restart()
|
||||
@@ -579,7 +650,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
if hasattr(self, '_steam_restart_progress'):
|
||||
self._steam_restart_progress.close()
|
||||
del self._steam_restart_progress
|
||||
self.setEnabled(True)
|
||||
self._enable_controls_after_operation()
|
||||
if success:
|
||||
self._safe_append_text("Steam restarted successfully.")
|
||||
|
||||
@@ -722,7 +793,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
"""Handle error from the automated prefix workflow"""
|
||||
self._safe_append_text(f"Error during automated Steam setup: {error_message}")
|
||||
self._safe_append_text("Please check the logs for details.")
|
||||
self.start_btn.setEnabled(True)
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
def show_shortcut_conflict_dialog(self, conflicts):
|
||||
"""Show dialog to resolve shortcut name conflicts"""
|
||||
@@ -1162,8 +1233,8 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
|
||||
def on_configuration_complete(self, success, message, modlist_name):
|
||||
"""Handle configuration completion (same as Tuxborn)"""
|
||||
# Always re-enable the start button when workflow completes
|
||||
self.start_btn.setEnabled(True)
|
||||
# Re-enable all controls when workflow completes
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
if success:
|
||||
# Calculate time taken
|
||||
@@ -1185,8 +1256,8 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
|
||||
def on_configuration_error(self, error_message):
|
||||
"""Handle configuration error"""
|
||||
# Re-enable the start button on error
|
||||
self.start_btn.setEnabled(True)
|
||||
# Re-enable all controls on error
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
self._safe_append_text(f"Configuration error: {error_message}")
|
||||
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium")
|
||||
|
||||
@@ -355,9 +355,8 @@ class InstallModlistScreen(QWidget):
|
||||
self.online_modlists = {} # {game_type: [modlist_dict, ...]}
|
||||
self.modlist_details = {} # {modlist_name: modlist_dict}
|
||||
|
||||
# Path for workflow log
|
||||
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Modlist_Install_workflow.log')
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
# Initialize log path (can be refreshed via refresh_paths method)
|
||||
self.refresh_paths()
|
||||
|
||||
# Initialize services early
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
@@ -459,11 +458,11 @@ class InstallModlistScreen(QWidget):
|
||||
file_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.file_edit = QLineEdit()
|
||||
self.file_edit.setMinimumWidth(400)
|
||||
file_btn = QPushButton("Browse")
|
||||
file_btn.clicked.connect(self.browse_wabbajack_file)
|
||||
self.file_btn = QPushButton("Browse")
|
||||
self.file_btn.clicked.connect(self.browse_wabbajack_file)
|
||||
file_layout.addWidget(QLabel(".wabbajack File:"))
|
||||
file_layout.addWidget(self.file_edit)
|
||||
file_layout.addWidget(file_btn)
|
||||
file_layout.addWidget(self.file_btn)
|
||||
self.file_group.setLayout(file_layout)
|
||||
file_tab_vbox.addWidget(self.file_group)
|
||||
file_tab.setLayout(file_tab_vbox)
|
||||
@@ -484,22 +483,22 @@ class InstallModlistScreen(QWidget):
|
||||
install_dir_label = QLabel("Install Directory:")
|
||||
self.install_dir_edit = QLineEdit(self.config_handler.get_modlist_install_base_dir())
|
||||
self.install_dir_edit.setMaximumHeight(25) # Force compact height
|
||||
browse_install_btn = QPushButton("Browse")
|
||||
browse_install_btn.clicked.connect(self.browse_install_dir)
|
||||
self.browse_install_btn = QPushButton("Browse")
|
||||
self.browse_install_btn.clicked.connect(self.browse_install_dir)
|
||||
install_dir_hbox = QHBoxLayout()
|
||||
install_dir_hbox.addWidget(self.install_dir_edit)
|
||||
install_dir_hbox.addWidget(browse_install_btn)
|
||||
install_dir_hbox.addWidget(self.browse_install_btn)
|
||||
form_grid.addWidget(install_dir_label, 1, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
|
||||
form_grid.addLayout(install_dir_hbox, 1, 1)
|
||||
# Downloads Dir
|
||||
downloads_dir_label = QLabel("Downloads Directory:")
|
||||
self.downloads_dir_edit = QLineEdit(self.config_handler.get_modlist_downloads_base_dir())
|
||||
self.downloads_dir_edit.setMaximumHeight(25) # Force compact height
|
||||
browse_downloads_btn = QPushButton("Browse")
|
||||
browse_downloads_btn.clicked.connect(self.browse_downloads_dir)
|
||||
self.browse_downloads_btn = QPushButton("Browse")
|
||||
self.browse_downloads_btn.clicked.connect(self.browse_downloads_dir)
|
||||
downloads_dir_hbox = QHBoxLayout()
|
||||
downloads_dir_hbox.addWidget(self.downloads_dir_edit)
|
||||
downloads_dir_hbox.addWidget(browse_downloads_btn)
|
||||
downloads_dir_hbox.addWidget(self.browse_downloads_btn)
|
||||
form_grid.addWidget(downloads_dir_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
|
||||
form_grid.addLayout(downloads_dir_hbox, 2, 1)
|
||||
# Nexus API Key
|
||||
@@ -603,7 +602,25 @@ class InstallModlistScreen(QWidget):
|
||||
self.resolution_combo.setCurrentIndex(0)
|
||||
# Otherwise, default is 'Leave unchanged' (index 0)
|
||||
form_grid.addWidget(resolution_label, 5, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
|
||||
form_grid.addWidget(self.resolution_combo, 5, 1)
|
||||
|
||||
# Horizontal layout for resolution dropdown and auto-restart checkbox
|
||||
resolution_and_restart_layout = QHBoxLayout()
|
||||
resolution_and_restart_layout.setSpacing(12)
|
||||
|
||||
# Resolution dropdown (made smaller)
|
||||
self.resolution_combo.setMaximumWidth(280) # Constrain width but keep aesthetically pleasing
|
||||
resolution_and_restart_layout.addWidget(self.resolution_combo)
|
||||
|
||||
# Add stretch to push checkbox to the right
|
||||
resolution_and_restart_layout.addStretch()
|
||||
|
||||
# Auto-accept Steam restart checkbox (right-aligned)
|
||||
self.auto_restart_checkbox = QCheckBox("Auto-accept Steam restart")
|
||||
self.auto_restart_checkbox.setChecked(False) # Always default to unchecked per session
|
||||
self.auto_restart_checkbox.setToolTip("When checked, Steam restart dialog will be automatically accepted, allowing unattended installation")
|
||||
resolution_and_restart_layout.addWidget(self.auto_restart_checkbox)
|
||||
|
||||
form_grid.addLayout(resolution_and_restart_layout, 5, 1)
|
||||
form_section_widget = QWidget()
|
||||
form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
form_section_widget.setLayout(form_grid)
|
||||
@@ -723,6 +740,57 @@ class InstallModlistScreen(QWidget):
|
||||
|
||||
# Initialize process tracking
|
||||
self.process = None
|
||||
|
||||
# Initialize empty controls list - will be populated after UI is built
|
||||
self._actionable_controls = []
|
||||
|
||||
# Now collect all actionable controls after UI is fully built
|
||||
self._collect_actionable_controls()
|
||||
|
||||
def _collect_actionable_controls(self):
|
||||
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
|
||||
self._actionable_controls = [
|
||||
# Main action button
|
||||
self.start_btn,
|
||||
# Game/modlist selection
|
||||
self.game_type_btn,
|
||||
self.modlist_btn,
|
||||
# Source tabs (entire tab widget)
|
||||
self.source_tabs,
|
||||
# Form fields
|
||||
self.modlist_name_edit,
|
||||
self.install_dir_edit,
|
||||
self.downloads_dir_edit,
|
||||
self.api_key_edit,
|
||||
self.file_edit,
|
||||
# Browse buttons
|
||||
self.browse_install_btn,
|
||||
self.browse_downloads_btn,
|
||||
self.file_btn,
|
||||
# Resolution controls
|
||||
self.resolution_combo,
|
||||
# Checkboxes
|
||||
self.save_api_key_checkbox,
|
||||
self.auto_restart_checkbox,
|
||||
]
|
||||
|
||||
def _disable_controls_during_operation(self):
|
||||
"""Disable all actionable controls during install/configure operations (except Cancel)"""
|
||||
for control in self._actionable_controls:
|
||||
if control:
|
||||
control.setEnabled(False)
|
||||
|
||||
def _enable_controls_after_operation(self):
|
||||
"""Re-enable all actionable controls after install/configure operations complete"""
|
||||
for control in self._actionable_controls:
|
||||
if control:
|
||||
control.setEnabled(True)
|
||||
|
||||
def refresh_paths(self):
|
||||
"""Refresh cached paths when config changes."""
|
||||
from jackify.shared.paths import get_jackify_logs_dir
|
||||
self.modlist_log_path = get_jackify_logs_dir() / 'Modlist_Install_workflow.log'
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
|
||||
def _open_url_safe(self, url):
|
||||
"""Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller"""
|
||||
@@ -1121,6 +1189,9 @@ class InstallModlistScreen(QWidget):
|
||||
if not self._check_protontricks():
|
||||
return
|
||||
|
||||
# Disable all controls during installation (except Cancel)
|
||||
self._disable_controls_during_operation()
|
||||
|
||||
try:
|
||||
tab_index = self.source_tabs.currentIndex()
|
||||
install_mode = 'online'
|
||||
@@ -1128,12 +1199,14 @@ class InstallModlistScreen(QWidget):
|
||||
modlist = self.file_edit.text().strip()
|
||||
if not modlist or not os.path.isfile(modlist) or not modlist.endswith('.wabbajack'):
|
||||
MessageService.warning(self, "Invalid Modlist", "Please select a valid .wabbajack file.")
|
||||
self._enable_controls_after_operation()
|
||||
return
|
||||
install_mode = 'file'
|
||||
else:
|
||||
modlist = self.modlist_btn.text().strip()
|
||||
if not modlist or modlist in ("Select Modlist", "Fetching modlists...", "No modlists found.", "Error fetching modlists."):
|
||||
MessageService.warning(self, "Invalid Modlist", "Please select a valid modlist.")
|
||||
self._enable_controls_after_operation()
|
||||
return
|
||||
|
||||
# For online modlists, use machine_url instead of display name
|
||||
@@ -1159,6 +1232,7 @@ class InstallModlistScreen(QWidget):
|
||||
missing_fields.append("Nexus API Key")
|
||||
if missing_fields:
|
||||
MessageService.warning(self, "Missing Required Fields", f"Please fill in all required fields before starting the install:\n- " + "\n- ".join(missing_fields))
|
||||
self._enable_controls_after_operation()
|
||||
return
|
||||
validation_handler = ValidationHandler()
|
||||
from pathlib import Path
|
||||
@@ -1324,14 +1398,11 @@ class InstallModlistScreen(QWidget):
|
||||
debug_print(f"DEBUG: Exception in validate_and_start_install: {e}")
|
||||
import traceback
|
||||
debug_print(f"DEBUG: Traceback: {traceback.format_exc()}")
|
||||
# Re-enable the button in case of exception
|
||||
self.start_btn.setEnabled(True)
|
||||
# Re-enable all controls after exception
|
||||
self._enable_controls_after_operation()
|
||||
self.cancel_btn.setVisible(True)
|
||||
self.cancel_install_btn.setVisible(False)
|
||||
# Also re-enable the entire widget
|
||||
self.setEnabled(True)
|
||||
debug_print(f"DEBUG: Widget re-enabled in exception handler, widget enabled: {self.isEnabled()}")
|
||||
print(f"DEBUG: Widget re-enabled in exception handler, widget enabled: {self.isEnabled()}") # Always print
|
||||
debug_print(f"DEBUG: Controls re-enabled in exception handler")
|
||||
|
||||
def run_modlist_installer(self, modlist, install_dir, downloads_dir, api_key, install_mode='online'):
|
||||
debug_print('DEBUG: run_modlist_installer called - USING THREADED BACKEND WRAPPER')
|
||||
@@ -1501,12 +1572,21 @@ class InstallModlistScreen(QWidget):
|
||||
self._safe_append_text(f"\nModlist installation completed successfully.")
|
||||
self._safe_append_text(f"\nWarning: Post-install configuration skipped for unsupported game: {game_name or game_type}")
|
||||
else:
|
||||
# Show the normal install complete dialog for supported games
|
||||
reply = MessageService.question(
|
||||
self, "Modlist Install Complete!",
|
||||
"Modlist install complete!\n\nWould you like to add this modlist to Steam and configure it now? Steam will restart, closing any game you have open!",
|
||||
critical=False # Non-critical, won't steal focus
|
||||
)
|
||||
# Check if auto-restart is enabled
|
||||
auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked()
|
||||
|
||||
if auto_restart_enabled:
|
||||
# Auto-accept Steam restart - proceed without dialog
|
||||
self._safe_append_text("\nAuto-accepting Steam restart (unattended mode enabled)")
|
||||
reply = QMessageBox.Yes # Simulate user clicking Yes
|
||||
else:
|
||||
# Show the normal install complete dialog for supported games
|
||||
reply = MessageService.question(
|
||||
self, "Modlist Install Complete!",
|
||||
"Modlist install complete!\n\nWould you like to add this modlist to Steam and configure it now? Steam will restart, closing any game you have open!",
|
||||
critical=False # Non-critical, won't steal focus
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
# --- Create Steam shortcut BEFORE restarting Steam ---
|
||||
# Proceed directly to automated prefix creation
|
||||
@@ -1522,6 +1602,8 @@ class InstallModlistScreen(QWidget):
|
||||
"You can manually add the modlist to Steam later if desired.",
|
||||
safety_level="medium"
|
||||
)
|
||||
# Re-enable controls since operation is complete
|
||||
self._enable_controls_after_operation()
|
||||
else:
|
||||
# Check for user cancellation first
|
||||
last_output = self.console.toPlainText()
|
||||
@@ -1611,9 +1693,6 @@ class InstallModlistScreen(QWidget):
|
||||
progress.setMinimumDuration(0)
|
||||
progress.setValue(0)
|
||||
progress.show()
|
||||
self.setEnabled(False)
|
||||
debug_print(f"DEBUG: Widget disabled in restart_steam_and_configure, widget enabled: {self.isEnabled()}")
|
||||
print(f"DEBUG: Widget disabled in restart_steam_and_configure, widget enabled: {self.isEnabled()}") # Always print
|
||||
|
||||
def do_restart():
|
||||
debug_print("DEBUG: do_restart thread started - using direct backend service")
|
||||
@@ -1651,9 +1730,7 @@ class InstallModlistScreen(QWidget):
|
||||
finally:
|
||||
self._steam_restart_progress = None
|
||||
|
||||
self.setEnabled(True)
|
||||
debug_print(f"DEBUG: Widget re-enabled in _on_steam_restart_finished, widget enabled: {self.isEnabled()}")
|
||||
print(f"DEBUG: Widget re-enabled in _on_steam_restart_finished, widget enabled: {self.isEnabled()}") # Always print
|
||||
# Controls are managed by the proper control management system
|
||||
if success:
|
||||
self._safe_append_text("Steam restarted successfully.")
|
||||
|
||||
@@ -1676,6 +1753,8 @@ class InstallModlistScreen(QWidget):
|
||||
def start_automated_prefix_workflow(self):
|
||||
"""Start the automated prefix creation workflow"""
|
||||
try:
|
||||
# Disable controls during installation
|
||||
self._disable_controls_during_operation()
|
||||
modlist_name = self.modlist_name_edit.text().strip()
|
||||
install_dir = self.install_dir_edit.text().strip()
|
||||
final_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
|
||||
@@ -1784,33 +1863,43 @@ class InstallModlistScreen(QWidget):
|
||||
import traceback
|
||||
debug_print(f"DEBUG: Traceback: {traceback.format_exc()}")
|
||||
self._safe_append_text(f"ERROR: Failed to start automated workflow: {e}")
|
||||
# Re-enable controls on exception
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
def on_automated_prefix_finished(self, success, prefix_path, new_appid_str, last_timestamp=None):
|
||||
"""Handle completion of automated prefix creation"""
|
||||
if success:
|
||||
debug_print(f"SUCCESS: Automated prefix creation completed!")
|
||||
debug_print(f"Prefix created at: {prefix_path}")
|
||||
if new_appid_str and new_appid_str != "0":
|
||||
debug_print(f"AppID: {new_appid_str}")
|
||||
|
||||
# Convert string AppID back to integer for configuration
|
||||
new_appid = int(new_appid_str) if new_appid_str and new_appid_str != "0" else None
|
||||
|
||||
# Continue with configuration using the new AppID and timestamp
|
||||
modlist_name = self.modlist_name_edit.text().strip()
|
||||
install_dir = self.install_dir_edit.text().strip()
|
||||
self.continue_configuration_after_automated_prefix(new_appid, modlist_name, install_dir, last_timestamp)
|
||||
else:
|
||||
self._safe_append_text(f"ERROR: Automated prefix creation failed")
|
||||
self._safe_append_text("Please check the logs for details")
|
||||
MessageService.critical(self, "Automated Setup Failed",
|
||||
"Automated prefix creation failed. Please check the console output for details.")
|
||||
try:
|
||||
if success:
|
||||
debug_print(f"SUCCESS: Automated prefix creation completed!")
|
||||
debug_print(f"Prefix created at: {prefix_path}")
|
||||
if new_appid_str and new_appid_str != "0":
|
||||
debug_print(f"AppID: {new_appid_str}")
|
||||
|
||||
# Convert string AppID back to integer for configuration
|
||||
new_appid = int(new_appid_str) if new_appid_str and new_appid_str != "0" else None
|
||||
|
||||
# Continue with configuration using the new AppID and timestamp
|
||||
modlist_name = self.modlist_name_edit.text().strip()
|
||||
install_dir = self.install_dir_edit.text().strip()
|
||||
self.continue_configuration_after_automated_prefix(new_appid, modlist_name, install_dir, last_timestamp)
|
||||
else:
|
||||
self._safe_append_text(f"ERROR: Automated prefix creation failed")
|
||||
self._safe_append_text("Please check the logs for details")
|
||||
MessageService.critical(self, "Automated Setup Failed",
|
||||
"Automated prefix creation failed. Please check the console output for details.")
|
||||
# Re-enable controls on failure
|
||||
self._enable_controls_after_operation()
|
||||
finally:
|
||||
# Always ensure controls are re-enabled when workflow truly completes
|
||||
pass
|
||||
|
||||
def on_automated_prefix_error(self, error_msg):
|
||||
"""Handle error in automated prefix creation"""
|
||||
self._safe_append_text(f"ERROR: Error during automated prefix creation: {error_msg}")
|
||||
MessageService.critical(self, "Automated Setup Error",
|
||||
f"Error during automated prefix creation: {error_msg}")
|
||||
# Re-enable controls on error
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
def on_automated_prefix_progress(self, progress_msg):
|
||||
"""Handle progress updates from automated prefix creation"""
|
||||
@@ -1831,7 +1920,6 @@ class InstallModlistScreen(QWidget):
|
||||
self.steam_restart_progress.setMinimumDuration(0)
|
||||
self.steam_restart_progress.setValue(0)
|
||||
self.steam_restart_progress.show()
|
||||
self.setEnabled(False)
|
||||
|
||||
def hide_steam_restart_progress(self):
|
||||
"""Hide Steam restart progress dialog"""
|
||||
@@ -1843,45 +1931,53 @@ class InstallModlistScreen(QWidget):
|
||||
pass
|
||||
finally:
|
||||
self.steam_restart_progress = None
|
||||
self.setEnabled(True)
|
||||
# Controls are managed by the proper control management system
|
||||
|
||||
def on_configuration_complete(self, success, message, modlist_name):
|
||||
"""Handle configuration completion on main thread"""
|
||||
if success:
|
||||
# Show celebration SuccessDialog after the entire workflow
|
||||
from ..dialogs import SuccessDialog
|
||||
import time
|
||||
if not hasattr(self, '_install_workflow_start_time'):
|
||||
self._install_workflow_start_time = time.time()
|
||||
time_taken = int(time.time() - self._install_workflow_start_time)
|
||||
mins, secs = divmod(time_taken, 60)
|
||||
time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds"
|
||||
display_names = {
|
||||
'skyrim': 'Skyrim',
|
||||
'fallout4': 'Fallout 4',
|
||||
'falloutnv': 'Fallout New Vegas',
|
||||
'oblivion': 'Oblivion',
|
||||
'starfield': 'Starfield',
|
||||
'oblivion_remastered': 'Oblivion Remastered',
|
||||
'enderal': 'Enderal'
|
||||
}
|
||||
game_name = display_names.get(self._current_game_type, self._current_game_name)
|
||||
success_dialog = SuccessDialog(
|
||||
modlist_name=modlist_name,
|
||||
workflow_type="install",
|
||||
time_taken=time_str,
|
||||
game_name=game_name,
|
||||
parent=self
|
||||
)
|
||||
success_dialog.show()
|
||||
elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3:
|
||||
# Max retries reached - show failure message
|
||||
MessageService.critical(self, "Manual Steps Failed",
|
||||
"Manual steps validation failed after multiple attempts.")
|
||||
else:
|
||||
# Configuration failed for other reasons
|
||||
MessageService.critical(self, "Configuration Failed",
|
||||
"Post-install configuration failed. Please check the console output.")
|
||||
try:
|
||||
# Re-enable controls now that installation/configuration is complete
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
if success:
|
||||
# Show celebration SuccessDialog after the entire workflow
|
||||
from ..dialogs import SuccessDialog
|
||||
import time
|
||||
if not hasattr(self, '_install_workflow_start_time'):
|
||||
self._install_workflow_start_time = time.time()
|
||||
time_taken = int(time.time() - self._install_workflow_start_time)
|
||||
mins, secs = divmod(time_taken, 60)
|
||||
time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds"
|
||||
display_names = {
|
||||
'skyrim': 'Skyrim',
|
||||
'fallout4': 'Fallout 4',
|
||||
'falloutnv': 'Fallout New Vegas',
|
||||
'oblivion': 'Oblivion',
|
||||
'starfield': 'Starfield',
|
||||
'oblivion_remastered': 'Oblivion Remastered',
|
||||
'enderal': 'Enderal'
|
||||
}
|
||||
game_name = display_names.get(self._current_game_type, self._current_game_name)
|
||||
success_dialog = SuccessDialog(
|
||||
modlist_name=modlist_name,
|
||||
workflow_type="install",
|
||||
time_taken=time_str,
|
||||
game_name=game_name,
|
||||
parent=self
|
||||
)
|
||||
success_dialog.show()
|
||||
elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3:
|
||||
# Max retries reached - show failure message
|
||||
MessageService.critical(self, "Manual Steps Failed",
|
||||
"Manual steps validation failed after multiple attempts.")
|
||||
else:
|
||||
# Configuration failed for other reasons
|
||||
MessageService.critical(self, "Configuration Failed",
|
||||
"Post-install configuration failed. Please check the console output.")
|
||||
except Exception as e:
|
||||
# Ensure controls are re-enabled even on unexpected errors
|
||||
self._enable_controls_after_operation()
|
||||
raise
|
||||
# Clean up thread
|
||||
if hasattr(self, 'config_thread') and self.config_thread is not None:
|
||||
# Disconnect all signals to prevent "Internal C++ object already deleted" errors
|
||||
@@ -1940,8 +2036,8 @@ class InstallModlistScreen(QWidget):
|
||||
else:
|
||||
# User clicked Cancel or closed the dialog - cancel the workflow
|
||||
self._safe_append_text("\n🛑 Manual steps cancelled by user. Workflow stopped.")
|
||||
# Reset button states
|
||||
self.start_btn.setEnabled(True)
|
||||
# Re-enable all controls when workflow is cancelled
|
||||
self._enable_controls_after_operation()
|
||||
self.cancel_btn.setVisible(True)
|
||||
self.cancel_install_btn.setVisible(False)
|
||||
|
||||
@@ -2513,8 +2609,8 @@ class InstallModlistScreen(QWidget):
|
||||
# Cleanup any remaining processes
|
||||
self.cleanup_processes()
|
||||
|
||||
# Reset button states
|
||||
self.start_btn.setEnabled(True)
|
||||
# Reset button states and re-enable all controls
|
||||
self._enable_controls_after_operation()
|
||||
self.cancel_btn.setVisible(True)
|
||||
self.cancel_install_btn.setVisible(False)
|
||||
|
||||
|
||||
@@ -106,8 +106,7 @@ class TuxbornInstallerScreen(QWidget):
|
||||
self.modlist_details = {} # {modlist_name: modlist_dict}
|
||||
|
||||
# Path for workflow log
|
||||
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Tuxborn_Installer_workflow.log')
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
self.refresh_paths()
|
||||
|
||||
# Initialize services early
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
@@ -440,6 +439,12 @@ class TuxbornInstallerScreen(QWidget):
|
||||
self.start_btn.clicked.connect(self.validate_and_start_install)
|
||||
self.steam_restart_finished.connect(self._on_steam_restart_finished)
|
||||
|
||||
def refresh_paths(self):
|
||||
"""Refresh cached paths when config changes."""
|
||||
from jackify.shared.paths import get_jackify_logs_dir
|
||||
self.modlist_log_path = get_jackify_logs_dir() / 'Tuxborn_Installer_workflow.log'
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
|
||||
def _open_url_safe(self, url):
|
||||
"""Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller"""
|
||||
import subprocess
|
||||
|
||||
Reference in New Issue
Block a user