Sync from development - prepare for v0.2.0

This commit is contained in:
Omni
2025-12-06 20:09:55 +00:00
parent fe14e4ecfb
commit ce969eba1b
277 changed files with 14059 additions and 3899 deletions

View File

@@ -17,6 +17,7 @@ from PySide6.QtGui import QFont
from jackify.backend.models.configuration import SystemInfo
from ..shared_theme import JACKIFY_COLOR_BLUE
from ..utils import set_responsive_minimum
logger = logging.getLogger(__name__)
@@ -35,8 +36,8 @@ class AdditionalTasksScreen(QWidget):
def _setup_ui(self):
"""Set up the user interface following ModlistTasksScreen pattern"""
layout = QVBoxLayout()
layout.setContentsMargins(40, 40, 40, 40)
layout.setSpacing(0)
layout.setContentsMargins(30, 30, 30, 30) # Reduced from 40
layout.setSpacing(12) # Match main menu spacing
# Header section
self._setup_header(layout)
@@ -50,55 +51,58 @@ class AdditionalTasksScreen(QWidget):
def _setup_header(self, layout):
"""Set up the header section"""
header_widget = QWidget()
header_layout = QVBoxLayout()
header_layout.setSpacing(0)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(2)
# Title
title = QLabel("<b>Additional Tasks & Tools</b>")
title.setStyleSheet(f"font-size: 24px; color: {JACKIFY_COLOR_BLUE};")
title.setStyleSheet(f"font-size: 20px; color: {JACKIFY_COLOR_BLUE};")
title.setAlignment(Qt.AlignHCenter)
header_layout.addWidget(title)
# Add a spacer to match main menu vertical spacing
header_layout.addSpacing(16)
# Description
desc = QLabel(
"TTW automation and additional tools.<br>&nbsp;"
)
header_layout.addSpacing(10)
# Description area with fixed height
desc = QLabel("TTW automation and additional tools.")
desc.setWordWrap(True)
desc.setStyleSheet("color: #ccc;")
desc.setStyleSheet("color: #ccc; font-size: 13px;")
desc.setAlignment(Qt.AlignHCenter)
desc.setMaximumHeight(50) # Fixed height for description zone
header_layout.addWidget(desc)
header_layout.addSpacing(24)
# Separator (shorter like main menu)
header_layout.addSpacing(12)
# Separator
sep = QLabel()
sep.setFixedHeight(2)
sep.setFixedWidth(400) # Match button width
sep.setStyleSheet("background: #fff;")
header_layout.addWidget(sep, alignment=Qt.AlignHCenter)
header_layout.addSpacing(16)
layout.addLayout(header_layout)
header_layout.addSpacing(10)
header_widget.setLayout(header_layout)
header_widget.setFixedHeight(120) # Fixed total header height
layout.addWidget(header_widget)
def _setup_menu_buttons(self, layout):
"""Set up the menu buttons section"""
# Menu options - ONLY TTW and placeholder
MENU_ITEMS = [
("Install TTW", "ttw_install", "Install Tale of Two Wastelands using Hoolamike automation"),
("Install TTW", "ttw_install", "Install Tale of Two Wastelands using TTW_Linux_Installer"),
("Coming Soon...", "coming_soon", "Additional tools will be added in future updates"),
("Return to Main Menu", "return_main_menu", "Go back to the main menu"),
]
# Create grid layout for buttons (mirror ModlistTasksScreen pattern)
button_grid = QGridLayout()
button_grid.setSpacing(16)
button_grid.setSpacing(12) # Reduced from 16
button_grid.setAlignment(Qt.AlignHCenter)
button_width = 400
button_height = 50
button_height = 40 # Reduced from 50
for i, (label, action_id, description) in enumerate(MENU_ITEMS):
# Create button
@@ -109,8 +113,8 @@ class AdditionalTasksScreen(QWidget):
background-color: #4a5568;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
border-radius: 6px;
font-size: 13px;
font-weight: bold;
text-align: center;
}}
@@ -126,7 +130,7 @@ class AdditionalTasksScreen(QWidget):
# Description label
desc_label = QLabel(description)
desc_label.setAlignment(Qt.AlignHCenter)
desc_label.setStyleSheet("color: #999; font-size: 12px;")
desc_label.setStyleSheet("color: #999; font-size: 11px;") # Reduced from 12px
desc_label.setWordWrap(True)
desc_label.setFixedWidth(button_width)
@@ -166,4 +170,18 @@ class AdditionalTasksScreen(QWidget):
def _return_to_main_menu(self):
"""Return to main menu"""
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(self.main_menu_index)
self.stacked_widget.setCurrentIndex(self.main_menu_index)
def showEvent(self, event):
"""Called when the widget becomes visible - resize to compact size"""
super().showEvent(event)
try:
main_window = self.window()
if main_window:
from PySide6.QtCore import QSize
# Only set minimum size - DO NOT RESIZE
main_window.setMaximumSize(QSize(16777215, 16777215))
set_responsive_minimum(main_window, min_width=960, min_height=420)
# DO NOT resize - let window stay at current size
except Exception:
pass

View File

@@ -3,7 +3,11 @@ from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
from ..utils import ansi_to_html
from ..utils import ansi_to_html, set_responsive_minimum
# Progress reporting components
from jackify.frontends.gui.widgets.progress_indicator import OverallProgressIndicator
from jackify.frontends.gui.widgets.file_progress_list import FileProgressList
from jackify.shared.progress_models import InstallationPhase, InstallationProgress
import os
import subprocess
import sys
@@ -50,25 +54,23 @@ class ConfigureExistingModlistScreen(QWidget):
self.resolution_service = ResolutionService()
self.config_handler = ConfigHandler()
# --- Fetch shortcuts for ModOrganizer.exe using existing backend functionality ---
# Use existing discover_executable_shortcuts which already filters by protontricks availability
from jackify.backend.handlers.modlist_handler import ModlistHandler
# Initialize modlist handler with empty config dict to use default initialization
modlist_handler = ModlistHandler({})
discovered_modlists = modlist_handler.discover_executable_shortcuts("ModOrganizer.exe")
# Convert to shortcut_handler format for UI compatibility
# --- Fetch shortcuts for ModOrganizer.exe - deferred to showEvent to avoid blocking init ---
# Initialize empty list, will be populated when screen is shown
self.mo2_shortcuts = []
for modlist in discovered_modlists:
# Convert discovered modlist format to shortcut format
shortcut = {
'AppName': modlist.get('name', 'Unknown'),
'AppID': modlist.get('appid', ''),
'StartDir': modlist.get('path', ''),
'Exe': f"{modlist.get('path', '')}/ModOrganizer.exe"
}
self.mo2_shortcuts.append(shortcut)
self._shortcuts_loaded = False
self._shortcut_loader = None # Thread for async shortcut loading
# Initialize progress reporting components
self.progress_indicator = OverallProgressIndicator(show_progress_bar=True)
self.progress_indicator.set_status("Ready to configure", 0)
self.file_progress_list = FileProgressList()
# Create "Show Details" checkbox
self.show_details_checkbox = QCheckBox("Show details")
self.show_details_checkbox.setChecked(False) # Start collapsed
self.show_details_checkbox.setToolTip("Toggle between activity summary and detailed console output")
self.show_details_checkbox.toggled.connect(self._on_show_details_toggled)
# --- UI Layout ---
main_overall_vbox = QVBoxLayout(self)
main_overall_vbox.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
@@ -211,77 +213,104 @@ class ConfigureExistingModlistScreen(QWidget):
self.start_btn = QPushButton("Start Configuration")
btn_row.addWidget(self.start_btn)
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.go_back)
cancel_btn.clicked.connect(self.cancel_and_cleanup)
btn_row.addWidget(cancel_btn)
user_config_widget = QWidget()
user_config_widget.setLayout(user_config_vbox)
if self.debug:
user_config_widget.setStyleSheet("border: 2px solid orange;")
user_config_widget.setToolTip("USER_CONFIG_WIDGET")
# Right: Activity window (FileProgressList widget)
# Fixed size policy to prevent shrinking when window expands
self.file_progress_list.setMinimumSize(QSize(300, 20))
self.file_progress_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
activity_widget = QWidget()
activity_layout = QVBoxLayout()
activity_layout.setContentsMargins(0, 0, 0, 0)
activity_layout.setSpacing(0)
activity_layout.addWidget(self.file_progress_list)
activity_widget.setLayout(activity_layout)
if self.debug:
activity_widget.setStyleSheet("border: 2px solid purple;")
activity_widget.setToolTip("ACTIVITY_WINDOW")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(activity_widget, stretch=9)
# Keep legacy process monitor hidden (for compatibility with existing code)
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;")
self.process_monitor_heading = QLabel("<b>[Process Monitor]</b>")
self.process_monitor_heading.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px; margin-bottom: 2px;")
self.process_monitor_heading.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
process_vbox = QVBoxLayout()
process_vbox.setContentsMargins(0, 0, 0, 0)
process_vbox.setSpacing(2)
process_vbox.addWidget(self.process_monitor_heading)
process_vbox.addWidget(self.process_monitor)
process_monitor_widget = QWidget()
process_monitor_widget.setLayout(process_vbox)
if self.debug:
process_monitor_widget.setStyleSheet("border: 2px solid purple;")
process_monitor_widget.setToolTip("PROCESS_MONITOR")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(process_monitor_widget, stretch=9)
self.process_monitor.setVisible(False) # Hidden in compact mode
upper_hbox.setAlignment(Qt.AlignTop)
upper_section_widget = QWidget()
upper_section_widget.setLayout(upper_hbox)
# Use Fixed size policy for consistent height
upper_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
upper_section_widget.setMaximumHeight(280) # Increased to show resolution dropdown
if self.debug:
upper_section_widget.setStyleSheet("border: 2px solid green;")
upper_section_widget.setToolTip("UPPER_SECTION")
main_overall_vbox.addWidget(upper_section_widget)
# Remove spacing - console should expand to fill available space
# Status banner with progress indicator and "Show details" toggle
banner_row = QHBoxLayout()
banner_row.setContentsMargins(0, 0, 0, 0)
banner_row.setSpacing(8)
banner_row.addWidget(self.progress_indicator, 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) # Compact height
banner_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
main_overall_vbox.addWidget(banner_row_widget)
# Console output area (shown when "Show details" is checked)
self.console = QTextEdit()
self.console.setReadOnly(True)
self.console.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
self.console.setMinimumHeight(50) # Very small minimum - can shrink to almost nothing
self.console.setMaximumHeight(1000) # Allow growth when space available
self.console.setMinimumHeight(50)
self.console.setMaximumHeight(1000)
self.console.setFontFamily('monospace')
self.console.setVisible(False) # Hidden by default (compact mode)
if self.debug:
self.console.setStyleSheet("border: 2px solid yellow;")
self.console.setToolTip("CONSOLE")
# Set up scroll tracking for professional auto-scroll behavior
self._setup_scroll_tracking()
# Wrap button row in widget for debug borders
btn_row_widget = QWidget()
btn_row_widget.setLayout(btn_row)
btn_row_widget.setMaximumHeight(50) # Limit height to make it more compact
btn_row_widget.setMaximumHeight(50)
if self.debug:
btn_row_widget.setStyleSheet("border: 2px solid red;")
btn_row_widget.setToolTip("BUTTON_ROW")
# Create a container that holds console + button row with proper spacing
console_and_buttons_widget = QWidget()
console_and_buttons_layout = QVBoxLayout()
console_and_buttons_layout.setContentsMargins(0, 0, 0, 0)
console_and_buttons_layout.setSpacing(8) # Small gap between console and buttons
console_and_buttons_layout.addWidget(self.console, stretch=1) # Console fills most space
console_and_buttons_layout.addWidget(btn_row_widget) # Buttons at bottom of this container
console_and_buttons_layout.setSpacing(8)
console_and_buttons_layout.addWidget(self.console, stretch=1)
console_and_buttons_layout.addWidget(btn_row_widget)
console_and_buttons_widget.setLayout(console_and_buttons_layout)
console_and_buttons_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
console_and_buttons_widget.setFixedHeight(50) # Lock to button row height when console is hidden
if self.debug:
console_and_buttons_widget.setStyleSheet("border: 2px solid lightblue;")
console_and_buttons_widget.setToolTip("CONSOLE_AND_BUTTONS_CONTAINER")
main_overall_vbox.addWidget(console_and_buttons_widget, stretch=1) # This container fills remaining space
# Add without stretch to prevent squashing upper section
main_overall_vbox.addWidget(console_and_buttons_widget)
# Store references for toggle functionality
self.console_and_buttons_widget = console_and_buttons_widget
self.console_and_buttons_layout = console_and_buttons_layout
self.main_overall_vbox = main_overall_vbox
self.setLayout(main_overall_vbox)
self.process = None
self.log_timer = None
@@ -379,6 +408,88 @@ class ConfigureExistingModlistScreen(QWidget):
if scrollbar.value() >= scrollbar.maximum() - 1:
self._user_manually_scrolled = False
def _on_show_details_toggled(self, checked):
"""Handle Show Details checkbox toggle"""
self._toggle_console_visibility(checked)
def _toggle_console_visibility(self, is_checked):
"""Toggle console visibility and window size"""
main_window = None
try:
parent = self.parent()
while parent and not isinstance(parent, QMainWindow):
parent = parent.parent()
if parent and isinstance(parent, QMainWindow):
main_window = parent
except Exception:
pass
if is_checked:
# Show console
self.console.setVisible(True)
self.console.setMinimumHeight(200)
self.console.setMaximumHeight(16777215)
self.console.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Allow expansion when console is visible
if hasattr(self, 'console_and_buttons_widget'):
self.console_and_buttons_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.console_and_buttons_widget.setMinimumHeight(0)
self.console_and_buttons_widget.setMaximumHeight(16777215)
self.console_and_buttons_widget.updateGeometry()
# Stop CPU tracking when showing console
self.file_progress_list.stop_cpu_tracking()
# Expand window
if main_window:
try:
from PySide6.QtCore import QSize
# On Steam Deck, keep fullscreen; on other systems, set normal window state
if not (hasattr(main_window, 'system_info') and main_window.system_info.is_steamdeck):
main_window.showNormal()
main_window.setMaximumHeight(16777215)
main_window.setMinimumHeight(0)
expanded_min = 900
current_size = main_window.size()
target_height = max(expanded_min, 900)
main_window.setMinimumHeight(expanded_min)
main_window.resize(current_size.width(), target_height)
self.main_overall_vbox.invalidate()
self.updateGeometry()
except Exception:
pass
else:
# Hide console
self.console.setVisible(False)
self.console.setMinimumHeight(0)
self.console.setMaximumHeight(0)
self.console.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
# Lock height when console is hidden
if hasattr(self, 'console_and_buttons_widget'):
self.console_and_buttons_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.console_and_buttons_widget.setFixedHeight(50)
self.console_and_buttons_widget.updateGeometry()
# CPU tracking will start when user clicks "Start Configuration", not here
# (Removed to avoid blocking showEvent)
# Collapse window
if main_window:
try:
from PySide6.QtCore import QSize
# Use fixed compact height for consistency across all workflow screens
compact_height = 620
# On Steam Deck, keep fullscreen; on other systems, set normal window state
if not (hasattr(main_window, 'system_info') and main_window.system_info.is_steamdeck):
main_window.showNormal()
set_responsive_minimum(main_window, min_width=960, min_height=compact_height)
current_size = main_window.size()
main_window.resize(current_size.width(), compact_height)
except Exception:
pass
def _safe_append_text(self, text):
"""Append text with professional auto-scroll behavior"""
# Write all messages to log file
@@ -420,7 +531,13 @@ class ConfigureExistingModlistScreen(QWidget):
from pathlib import Path
log_handler = LoggingHandler()
log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5)
# Initialize progress indicator
self.progress_indicator.set_status("Preparing to configure...", 0)
# Start CPU tracking
self.file_progress_list.start_cpu_tracking()
# Disable controls during configuration
self._disable_controls_during_operation()
@@ -560,7 +677,10 @@ class ConfigureExistingModlistScreen(QWidget):
if success:
# Calculate time taken
time_taken = self._calculate_time_taken()
# Clear Activity window before showing success dialog
self.file_progress_list.clear()
# Show success dialog with celebration
success_dialog = SuccessDialog(
modlist_name=modlist_name,
@@ -644,9 +764,188 @@ class ConfigureExistingModlistScreen(QWidget):
dlg.exec()
def go_back(self):
"""Navigate back to main menu and restore window size"""
# Emit collapse signal to restore compact mode
self.resize_request.emit('collapse')
# Restore window size before navigating away
try:
main_window = self.window()
if main_window:
from PySide6.QtCore import QSize
from ..utils import apply_window_size_and_position
# Only set minimum size - DO NOT RESIZE
main_window.setMaximumSize(QSize(16777215, 16777215))
set_responsive_minimum(main_window, min_width=960, min_height=420)
# DO NOT resize - let window stay at current size
except Exception:
pass
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(self.main_menu_index)
def cleanup_processes(self):
"""Clean up any running processes when the window closes or is cancelled"""
# Stop CPU tracking if active
if hasattr(self, 'file_progress_list'):
self.file_progress_list.stop_cpu_tracking()
# Clean up configuration thread if running
if hasattr(self, 'config_thread') and self.config_thread.isRunning():
self.config_thread.terminate()
self.config_thread.wait(1000)
def cancel_and_cleanup(self):
"""Handle Cancel button - clean up processes and go back"""
self.cleanup_processes()
self.go_back()
def showEvent(self, event):
"""Called when the widget becomes visible - ensure collapsed state"""
super().showEvent(event)
# Load shortcuts asynchronously (only once, on first show) to avoid blocking UI
if not self._shortcuts_loaded:
# Load in background thread to avoid blocking UI
from PySide6.QtCore import QTimer
QTimer.singleShot(0, self._load_shortcuts_async)
self._shortcuts_loaded = True
# Ensure initial collapsed layout each time this screen is opened
try:
from PySide6.QtCore import Qt as _Qt
# Ensure checkbox is unchecked without emitting signals
if self.show_details_checkbox.isChecked():
self.show_details_checkbox.blockSignals(True)
self.show_details_checkbox.setChecked(False)
self.show_details_checkbox.blockSignals(False)
# Force collapsed state
self._toggle_console_visibility(False)
# Only set minimum size - DO NOT RESIZE
main_window = self.window()
if main_window:
from PySide6.QtCore import QSize
main_window.setMaximumSize(QSize(16777215, 16777215))
set_responsive_minimum(main_window, min_width=960, min_height=420)
# DO NOT resize - let window stay at current size
except Exception as e:
# If initial collapse fails, log but don't crash
print(f"Warning: Failed to set initial collapsed state: {e}")
def hideEvent(self, event):
"""Clean up thread when screen is hidden"""
super().hideEvent(event)
# Clean up shortcut loader thread if it's still running
if self._shortcut_loader is not None:
if self._shortcut_loader.isRunning():
self._shortcut_loader.finished_signal.disconnect()
self._shortcut_loader.terminate()
self._shortcut_loader.wait(1000) # Wait up to 1 second for cleanup
self._shortcut_loader = None
def _load_shortcuts_async(self):
"""Load ModOrganizer.exe shortcuts asynchronously to avoid blocking UI"""
from PySide6.QtCore import QThread, Signal, QObject
class ShortcutLoaderThread(QThread):
finished_signal = Signal(list) # Emits list of shortcuts when done
error_signal = Signal(str) # Emits error message if something goes wrong
def run(self):
try:
# Suppress all logging/output in background thread to avoid reentrant stderr issues
import logging
import sys
# Temporarily redirect stderr to avoid reentrant calls
old_stderr = sys.stderr
try:
# Use a null device or StringIO to capture errors without writing to stderr
from io import StringIO
sys.stderr = StringIO()
# Fetch shortcuts for ModOrganizer.exe using existing backend functionality
from jackify.backend.handlers.modlist_handler import ModlistHandler
# Initialize modlist handler with empty config dict to use default initialization
modlist_handler = ModlistHandler({})
discovered_modlists = modlist_handler.discover_executable_shortcuts("ModOrganizer.exe")
# Convert to shortcut_handler format for UI compatibility
shortcuts = []
for modlist in discovered_modlists:
# Convert discovered modlist format to shortcut format
shortcut = {
'AppName': modlist.get('name', 'Unknown'),
'AppID': modlist.get('appid', ''),
'StartDir': modlist.get('path', ''),
'Exe': f"{modlist.get('path', '')}/ModOrganizer.exe"
}
shortcuts.append(shortcut)
# Restore stderr before emitting signal
sys.stderr = old_stderr
self.finished_signal.emit(shortcuts)
except Exception as inner_e:
# Restore stderr before emitting error
sys.stderr = old_stderr
error_msg = str(inner_e)
self.error_signal.emit(error_msg)
self.finished_signal.emit([])
except Exception as e:
# Fallback error handling
error_msg = str(e)
self.error_signal.emit(error_msg)
self.finished_signal.emit([])
# Show loading state in dropdown
if hasattr(self, 'shortcut_combo'):
self.shortcut_combo.clear()
self.shortcut_combo.addItem("Loading modlists...")
self.shortcut_combo.setEnabled(False)
# Clean up any existing thread first
if self._shortcut_loader is not None:
if self._shortcut_loader.isRunning():
self._shortcut_loader.finished_signal.disconnect()
self._shortcut_loader.terminate()
self._shortcut_loader.wait(1000) # Wait up to 1 second
self._shortcut_loader = None
# Start background thread
self._shortcut_loader = ShortcutLoaderThread()
self._shortcut_loader.finished_signal.connect(self._on_shortcuts_loaded)
self._shortcut_loader.error_signal.connect(self._on_shortcuts_error)
self._shortcut_loader.start()
def _on_shortcuts_loaded(self, shortcuts):
"""Update UI when shortcuts are loaded"""
self.mo2_shortcuts = shortcuts
# Update the dropdown
if hasattr(self, 'shortcut_combo'):
self.shortcut_combo.clear()
self.shortcut_combo.setEnabled(True)
self.shortcut_combo.addItem("Please Select...")
self.shortcut_map.clear()
for shortcut in self.mo2_shortcuts:
display = f"{shortcut.get('AppName', shortcut.get('appname', 'Unknown'))} ({shortcut.get('StartDir', shortcut.get('startdir', ''))})"
self.shortcut_combo.addItem(display)
self.shortcut_map.append(shortcut)
def _on_shortcuts_error(self, error_msg):
"""Handle errors from shortcut loading thread"""
# Log error from main thread (safe to write to stderr here)
debug_print(f"Warning: Failed to load shortcuts: {error_msg}")
# Update UI to show error state
if hasattr(self, 'shortcut_combo'):
self.shortcut_combo.clear()
self.shortcut_combo.setEnabled(True)
self.shortcut_combo.addItem("Error loading modlists - please try again")
def update_top_panel(self):
try:
result = subprocess.run([
@@ -693,43 +992,10 @@ class ConfigureExistingModlistScreen(QWidget):
pass
def refresh_modlist_list(self):
"""Refresh the modlist dropdown by re-detecting ModOrganizer.exe shortcuts"""
try:
# Re-detect shortcuts using existing backend functionality
from jackify.backend.handlers.modlist_handler import ModlistHandler
# Initialize modlist handler with empty config dict to use default initialization
modlist_handler = ModlistHandler({})
discovered_modlists = modlist_handler.discover_executable_shortcuts("ModOrganizer.exe")
# Convert to shortcut_handler format for UI compatibility
self.mo2_shortcuts = []
for modlist in discovered_modlists:
# Convert discovered modlist format to shortcut format
shortcut = {
'AppName': modlist.get('name', 'Unknown'),
'AppID': modlist.get('appid', ''),
'StartDir': modlist.get('path', ''),
'Exe': f"{modlist.get('path', '')}/ModOrganizer.exe"
}
self.mo2_shortcuts.append(shortcut)
# Clear and repopulate the combo box
self.shortcut_combo.clear()
self.shortcut_combo.addItem("Please Select...")
self.shortcut_map.clear()
for shortcut in self.mo2_shortcuts:
display = f"{shortcut.get('AppName', shortcut.get('appname', 'Unknown'))} ({shortcut.get('StartDir', shortcut.get('startdir', ''))})"
self.shortcut_combo.addItem(display)
self.shortcut_map.append(shortcut)
# Show feedback to user in UI only (don't write to log before workflow starts)
# Feedback is shown by the updated dropdown items
except Exception as e:
# Don't write to log file before workflow starts - just show error in UI
MessageService.warning(self, "Refresh Error", f"Failed to refresh modlist list: {e}", safety_level="low")
"""Refresh the modlist dropdown by re-detecting ModOrganizer.exe shortcuts (async)"""
# Use async loading to avoid blocking UI
self._shortcuts_loaded = False # Allow reload
self._load_shortcuts_async()
def _calculate_time_taken(self) -> str:
"""Calculate and format the time taken for the workflow"""

View File

@@ -1,11 +1,15 @@
"""
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, QCheckBox
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QCheckBox, QMainWindow
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
from ..utils import ansi_to_html
from ..utils import ansi_to_html, set_responsive_minimum
# Progress reporting components
from jackify.frontends.gui.widgets.progress_indicator import OverallProgressIndicator
from jackify.frontends.gui.widgets.file_progress_list import FileProgressList
from jackify.shared.progress_models import InstallationPhase, InstallationProgress
import os
import subprocess
import sys
@@ -44,17 +48,24 @@ class ModlistFetchThread(QThread):
self.install_dir = install_dir
self.download_dir = download_dir
def run(self):
# CRITICAL: Use safe Python executable to prevent AppImage recursive spawning
from jackify.backend.handlers.subprocess_utils import get_safe_python_executable
python_exe = get_safe_python_executable()
if self.mode == 'list-modlists':
cmd = [sys.executable, self.cli_path, '--install-modlist', '--list-modlists', '--game-type', self.game_type]
cmd = [python_exe, self.cli_path, '--install-modlist', '--list-modlists', '--game-type', self.game_type]
elif self.mode == 'install':
cmd = [sys.executable, self.cli_path, '--install-modlist', '--install', '--modlist-name', self.modlist_name, '--install-dir', self.install_dir, '--download-dir', self.download_dir, '--game-type', self.game_type]
cmd = [python_exe, self.cli_path, '--install-modlist', '--install', '--modlist-name', self.modlist_name, '--install-dir', self.install_dir, '--download-dir', self.download_dir, '--game-type', self.game_type]
else:
self.result.emit([], '[ModlistFetchThread] Unknown mode')
return
try:
with open(self.log_path, 'a') as logf:
logf.write(f"\n[Modlist Fetch CMD] {cmd}\n")
proc = subprocess.Popen(cmd, cwd=self.project_root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# Use clean subprocess environment to prevent AppImage variable inheritance
from jackify.backend.handlers.subprocess_utils import get_clean_subprocess_env
env = get_clean_subprocess_env()
proc = subprocess.Popen(cmd, cwd=self.project_root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)
stdout, stderr = proc.communicate()
logf.write(f"[stdout]\n{stdout}\n[stderr]\n{stderr}\n")
if proc.returncode == 0:
@@ -112,10 +123,21 @@ class ConfigureNewModlistScreen(QWidget):
# Scroll tracking for professional auto-scroll behavior
self._user_manually_scrolled = False
self._was_at_bottom = True
# Time tracking for workflow completion
self._workflow_start_time = None
# Initialize progress reporting components
self.progress_indicator = OverallProgressIndicator(show_progress_bar=True)
self.progress_indicator.set_status("Ready to configure", 0)
self.file_progress_list = FileProgressList()
# Create "Show Details" checkbox
self.show_details_checkbox = QCheckBox("Show details")
self.show_details_checkbox.setChecked(False) # Start collapsed
self.show_details_checkbox.setToolTip("Toggle between activity summary and detailed console output")
self.show_details_checkbox.toggled.connect(self._on_show_details_toggled)
main_overall_vbox = QVBoxLayout(self)
main_overall_vbox.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
main_overall_vbox.setContentsMargins(50, 50, 50, 0) # No bottom margin
@@ -271,79 +293,104 @@ class ConfigureNewModlistScreen(QWidget):
self.start_btn = QPushButton("Start Configuration")
btn_row.addWidget(self.start_btn)
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.go_back)
cancel_btn.clicked.connect(self.cancel_and_cleanup)
btn_row.addWidget(cancel_btn)
user_config_widget = QWidget()
user_config_widget.setLayout(user_config_vbox)
if self.debug:
user_config_widget.setStyleSheet("border: 2px solid orange;")
user_config_widget.setToolTip("USER_CONFIG_WIDGET")
# Right: process monitor (as before)
# Right: Activity window (FileProgressList widget)
# Fixed size policy to prevent shrinking when window expands
self.file_progress_list.setMinimumSize(QSize(300, 20))
self.file_progress_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
activity_widget = QWidget()
activity_layout = QVBoxLayout()
activity_layout.setContentsMargins(0, 0, 0, 0)
activity_layout.setSpacing(0)
activity_layout.addWidget(self.file_progress_list)
activity_widget.setLayout(activity_layout)
if self.debug:
activity_widget.setStyleSheet("border: 2px solid purple;")
activity_widget.setToolTip("ACTIVITY_WINDOW")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(activity_widget, stretch=9)
# Keep legacy process monitor hidden (for compatibility with existing code)
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;")
self.process_monitor_heading = QLabel("<b>[Process Monitor]</b>")
self.process_monitor_heading.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px; margin-bottom: 2px;")
self.process_monitor_heading.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
process_vbox = QVBoxLayout()
process_vbox.setContentsMargins(0, 0, 0, 0)
process_vbox.setSpacing(2)
process_vbox.addWidget(self.process_monitor_heading)
process_vbox.addWidget(self.process_monitor)
process_monitor_widget = QWidget()
process_monitor_widget.setLayout(process_vbox)
if self.debug:
process_monitor_widget.setStyleSheet("border: 2px solid purple;")
process_monitor_widget.setToolTip("PROCESS_MONITOR")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(process_monitor_widget, stretch=9)
self.process_monitor.setVisible(False) # Hidden in compact mode
upper_hbox.setAlignment(Qt.AlignTop)
upper_section_widget = QWidget()
upper_section_widget.setLayout(upper_hbox)
# Use Fixed size policy for consistent height
upper_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
upper_section_widget.setMaximumHeight(280) # Increased to show resolution dropdown
if self.debug:
upper_section_widget.setStyleSheet("border: 2px solid green;")
upper_section_widget.setToolTip("UPPER_SECTION")
main_overall_vbox.addWidget(upper_section_widget)
# Remove spacing - console should expand to fill available space
# --- Console output area (full width, placeholder for now) ---
# Status banner with progress indicator and "Show details" toggle
banner_row = QHBoxLayout()
banner_row.setContentsMargins(0, 0, 0, 0)
banner_row.setSpacing(8)
banner_row.addWidget(self.progress_indicator, 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) # Compact height
banner_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
main_overall_vbox.addWidget(banner_row_widget)
# Console output area (shown when "Show details" is checked)
self.console = QTextEdit()
self.console.setReadOnly(True)
self.console.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
self.console.setMinimumHeight(50) # Very small minimum - can shrink to almost nothing
self.console.setMaximumHeight(1000) # Allow growth when space available
self.console.setMinimumHeight(50)
self.console.setMaximumHeight(1000)
self.console.setFontFamily('monospace')
self.console.setVisible(False) # Hidden by default (compact mode)
if self.debug:
self.console.setStyleSheet("border: 2px solid yellow;")
self.console.setToolTip("CONSOLE")
# Set up scroll tracking for professional auto-scroll behavior
self._setup_scroll_tracking()
# Wrap button row in widget for debug borders
btn_row_widget = QWidget()
btn_row_widget.setLayout(btn_row)
btn_row_widget.setMaximumHeight(50) # Limit height to make it more compact
btn_row_widget.setMaximumHeight(50)
if self.debug:
btn_row_widget.setStyleSheet("border: 2px solid red;")
btn_row_widget.setToolTip("BUTTON_ROW")
# Create a container that holds console + button row with proper spacing
console_and_buttons_widget = QWidget()
console_and_buttons_layout = QVBoxLayout()
console_and_buttons_layout.setContentsMargins(0, 0, 0, 0)
console_and_buttons_layout.setSpacing(8) # Small gap between console and buttons
console_and_buttons_layout.addWidget(self.console, stretch=1) # Console fills most space
console_and_buttons_layout.addWidget(btn_row_widget) # Buttons at bottom of this container
console_and_buttons_layout.setSpacing(8)
console_and_buttons_layout.addWidget(self.console, stretch=1)
console_and_buttons_layout.addWidget(btn_row_widget)
console_and_buttons_widget.setLayout(console_and_buttons_layout)
console_and_buttons_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
console_and_buttons_widget.setFixedHeight(50) # Lock to button row height when console is hidden
if self.debug:
console_and_buttons_widget.setStyleSheet("border: 2px solid lightblue;")
console_and_buttons_widget.setToolTip("CONSOLE_AND_BUTTONS_CONTAINER")
main_overall_vbox.addWidget(console_and_buttons_widget, stretch=1) # This container fills remaining space
# Add without stretch to prevent squashing upper section
main_overall_vbox.addWidget(console_and_buttons_widget)
# Store references for toggle functionality
self.console_and_buttons_widget = console_and_buttons_widget
self.console_and_buttons_layout = console_and_buttons_layout
self.main_overall_vbox = main_overall_vbox
self.setLayout(main_overall_vbox)
# --- Process Monitor (right) ---
@@ -442,6 +489,87 @@ class ConfigureNewModlistScreen(QWidget):
if scrollbar.value() >= scrollbar.maximum() - 1:
self._user_manually_scrolled = False
def _on_show_details_toggled(self, checked):
"""Handle Show Details checkbox toggle"""
self._toggle_console_visibility(checked)
def _toggle_console_visibility(self, is_checked):
"""Toggle console visibility and window size"""
main_window = None
try:
parent = self.parent()
while parent and not isinstance(parent, QMainWindow):
parent = parent.parent()
if parent and isinstance(parent, QMainWindow):
main_window = parent
except Exception:
pass
if is_checked:
# Show console
self.console.setVisible(True)
self.console.setMinimumHeight(200)
self.console.setMaximumHeight(16777215)
self.console.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Allow expansion when console is visible
if hasattr(self, 'console_and_buttons_widget'):
self.console_and_buttons_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.console_and_buttons_widget.setMinimumHeight(0)
self.console_and_buttons_widget.setMaximumHeight(16777215)
self.console_and_buttons_widget.updateGeometry()
# Stop CPU tracking when showing console
self.file_progress_list.stop_cpu_tracking()
# Expand window
if main_window:
try:
from PySide6.QtCore import QSize
# On Steam Deck, keep fullscreen; on other systems, set normal window state
if not (hasattr(main_window, 'system_info') and main_window.system_info.is_steamdeck):
main_window.showNormal()
main_window.setMaximumHeight(16777215)
main_window.setMinimumHeight(0)
expanded_min = 900
current_size = main_window.size()
target_height = max(expanded_min, 900)
main_window.setMinimumHeight(expanded_min)
main_window.resize(current_size.width(), target_height)
self.main_overall_vbox.invalidate()
self.updateGeometry()
except Exception:
pass
else:
# Hide console
self.console.setVisible(False)
self.console.setMinimumHeight(0)
self.console.setMaximumHeight(0)
self.console.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
# Lock height when console is hidden
if hasattr(self, 'console_and_buttons_widget'):
self.console_and_buttons_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.console_and_buttons_widget.setFixedHeight(50)
self.console_and_buttons_widget.updateGeometry()
# CPU tracking will start when user clicks "Start Configuration", not here
# (Removed to avoid blocking showEvent)
# Collapse window
if main_window:
try:
from PySide6.QtCore import QSize
# Only set minimum size - DO NOT RESIZE
# On Steam Deck, keep fullscreen; on other systems, set normal window state
if not (hasattr(main_window, 'system_info') and main_window.system_info.is_steamdeck):
main_window.showNormal()
main_window.setMaximumSize(QSize(16777215, 16777215))
set_responsive_minimum(main_window, min_width=960, min_height=420)
# DO NOT resize - let window stay at current size
except Exception:
pass
def _safe_append_text(self, text):
"""Append text with professional auto-scroll behavior"""
# Write all messages to log file
@@ -480,9 +608,62 @@ class ConfigureNewModlistScreen(QWidget):
self.install_dir_edit.setText(file)
def go_back(self):
"""Navigate back to main menu and restore window size"""
# Emit collapse signal to restore compact mode
self.resize_request.emit('collapse')
# Restore window size before navigating away
try:
main_window = self.window()
if main_window:
from PySide6.QtCore import QSize
from ..utils import apply_window_size_and_position
# Only set minimum size - DO NOT RESIZE
main_window.setMaximumSize(QSize(16777215, 16777215))
set_responsive_minimum(main_window, min_width=960, min_height=420)
# DO NOT resize - let window stay at current size
except Exception:
pass
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(self.main_menu_index)
def cleanup_processes(self):
"""Clean up any running processes when the window closes or is cancelled"""
# Stop CPU tracking if active
if hasattr(self, 'file_progress_list'):
self.file_progress_list.stop_cpu_tracking()
# Clean up configuration thread if running
if hasattr(self, 'config_thread') and self.config_thread.isRunning():
self.config_thread.terminate()
self.config_thread.wait(1000)
def cancel_and_cleanup(self):
"""Handle Cancel button - clean up processes and go back"""
self.cleanup_processes()
self.go_back()
def showEvent(self, event):
"""Called when the widget becomes visible - ensure collapsed state"""
super().showEvent(event)
# Ensure initial collapsed layout each time this screen is opened
try:
from PySide6.QtCore import Qt as _Qt
# Ensure checkbox is unchecked without emitting signals
if self.show_details_checkbox.isChecked():
self.show_details_checkbox.blockSignals(True)
self.show_details_checkbox.setChecked(False)
self.show_details_checkbox.blockSignals(False)
# Force collapsed state
self._toggle_console_visibility(False)
except Exception as e:
# If initial collapse fails, log but don't crash
print(f"Warning: Failed to set initial collapsed state: {e}")
def update_top_panel(self):
try:
result = subprocess.run([
@@ -528,6 +709,9 @@ class ConfigureNewModlistScreen(QWidget):
def _check_protontricks(self):
"""Check if protontricks is available before critical operations"""
try:
if self.protontricks_service.is_bundled_mode():
return True
is_installed, installation_type, details = self.protontricks_service.detect_protontricks()
if not is_installed:
@@ -582,7 +766,13 @@ class ConfigureNewModlistScreen(QWidget):
# Start time tracking
self._workflow_start_time = time.time()
# Initialize progress indicator
self.progress_indicator.set_status("Preparing to configure...", 0)
# Start CPU tracking
self.file_progress_list.start_cpu_tracking()
# Disable controls during configuration (after validation passes)
self._disable_controls_during_operation()
@@ -1251,7 +1441,10 @@ class ConfigureNewModlistScreen(QWidget):
if success:
# Calculate time taken
time_taken = self._calculate_time_taken()
# Clear Activity window before showing success dialog
self.file_progress_list.clear()
# Show success dialog with celebration
success_dialog = SuccessDialog(
modlist_name=modlist_name,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
"""
Simplified output handler for TTW installation - minimal filtering, maximum stability
This is a reference implementation showing the absolute minimum needed.
"""
def on_installation_output_simple(self, message):
"""
Ultra-simplified output handler:
- Strip emojis (required)
- Show all output (no filtering)
- Extract progress numbers for Activity window only
- No regex except for simple number extraction
"""
# Strip ANSI codes
cleaned = strip_ansi_control_codes(message).strip()
# Strip emojis - character by character (no regex)
filtered_chars = []
for char in cleaned:
code = ord(char)
is_emoji = (
(0x1F300 <= code <= 0x1F9FF) or
(0x1F600 <= code <= 0x1F64F) or
(0x2600 <= code <= 0x26FF) or
(0x2700 <= code <= 0x27BF)
)
if not is_emoji:
filtered_chars.append(char)
cleaned = ''.join(filtered_chars).strip()
if not cleaned:
return
# Log everything
self._write_to_log_file(message)
# Show everything in console (no filtering)
self._safe_append_text(cleaned)
# Extract progress for Activity window ONLY - minimal regex with error handling
# Pattern: [X/Y] or "Loading manifest: X/Y"
try:
# Try to extract [X/Y] pattern
import re
match = re.search(r'\[(\d+)/(\d+)\]', cleaned)
if match:
current = int(match.group(1))
total = int(match.group(2))
percent = int((current / total) * 100) if total > 0 else 0
phase = self._ttw_current_phase or "Processing"
self._update_ttw_activity(current, total, percent)
# Try "Loading manifest: X/Y"
match = re.search(r'loading manifest:\s*(\d+)/(\d+)', cleaned.lower())
if match:
current = int(match.group(1))
total = int(match.group(2))
percent = int((current / total) * 100) if total > 0 else 0
self._ttw_current_phase = "Loading manifest"
self._update_ttw_activity(current, total, percent)
except (RecursionError, re.error, Exception):
# If regex fails, just skip progress extraction - show output anyway
pass

View File

@@ -6,6 +6,7 @@ from PySide6.QtGui import QPixmap, QFont
from PySide6.QtCore import Qt
import os
from ..shared_theme import JACKIFY_COLOR_BLUE, LOGO_PATH, DISCLAIMER_TEXT
from ..utils import set_responsive_minimum
class MainMenu(QWidget):
def __init__(self, stacked_widget=None, dev_mode=False):
@@ -14,37 +15,52 @@ class MainMenu(QWidget):
self.dev_mode = dev_mode
layout = QVBoxLayout()
layout.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
layout.setContentsMargins(50, 50, 50, 50)
layout.setSpacing(20)
layout.setContentsMargins(30, 30, 30, 30) # Reduced from 50
layout.setSpacing(12) # Reduced from 20
# Header zone with fixed height for consistent layout across all menu screens
header_widget = QWidget()
header_layout = QVBoxLayout()
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(2)
# Title
title = QLabel("<b>Jackify</b>")
title.setStyleSheet(f"font-size: 24px; color: {JACKIFY_COLOR_BLUE};")
title.setStyleSheet(f"font-size: 20px; color: {JACKIFY_COLOR_BLUE};")
title.setAlignment(Qt.AlignHCenter)
layout.addWidget(title)
header_layout.addWidget(title)
# Description
header_layout.addSpacing(10)
# Description area with fixed height
desc = QLabel(
"Manage your modlists with native Linux tools. "
"Choose from the options below to install, "
"configure, or manage modlists."
)
desc.setWordWrap(True)
desc.setStyleSheet("color: #ccc;")
desc.setStyleSheet("color: #ccc; font-size: 13px;")
desc.setAlignment(Qt.AlignHCenter)
layout.addWidget(desc)
desc.setMaximumHeight(50) # Fixed height for description zone
header_layout.addWidget(desc)
header_layout.addSpacing(12)
# Separator
layout.addSpacing(16)
sep = QLabel()
sep.setFixedHeight(2)
sep.setStyleSheet("background: #fff;")
layout.addWidget(sep)
layout.addSpacing(16)
header_layout.addWidget(sep)
header_layout.addSpacing(10)
header_widget.setLayout(header_layout)
header_widget.setFixedHeight(120) # Fixed total header height
layout.addWidget(header_widget)
# Menu buttons
button_width = 400
button_height = 60
button_height = 40 # Reduced from 50/60
MENU_ITEMS = [
("Modlist Tasks", "modlist_tasks", "Manage your modlists with native Linux tools"),
("Additional Tasks", "additional_tasks", "Additional Tasks & Tools, such as TTW Installation"),
@@ -54,14 +70,14 @@ class MainMenu(QWidget):
for label, action_id, description in MENU_ITEMS:
# Main button
btn = QPushButton(label)
btn.setFixedSize(button_width, 50)
btn.setFixedSize(button_width, button_height) # Use variable height
btn.setStyleSheet(f"""
QPushButton {{
background-color: #4a5568;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
border-radius: 6px;
font-size: 13px;
font-weight: bold;
text-align: center;
}}
@@ -73,28 +89,28 @@ class MainMenu(QWidget):
}}
""")
btn.clicked.connect(lambda checked, a=action_id: self.menu_action(a))
# Button container with proper alignment
btn_container = QWidget()
btn_layout = QVBoxLayout()
btn_layout.setContentsMargins(0, 0, 0, 0)
btn_layout.setSpacing(4)
btn_layout.setSpacing(3) # Reduced from 4
btn_layout.setAlignment(Qt.AlignHCenter)
btn_layout.addWidget(btn)
# Description label with proper alignment
desc_label = QLabel(description)
desc_label.setAlignment(Qt.AlignHCenter)
desc_label.setStyleSheet("color: #999; font-size: 12px;")
desc_label.setStyleSheet("color: #999; font-size: 11px;") # Reduced from 12px
desc_label.setWordWrap(True)
desc_label.setFixedWidth(button_width) # Match button width for proper alignment
desc_label.setFixedWidth(button_width)
btn_layout.addWidget(desc_label)
btn_container.setLayout(btn_layout)
layout.addWidget(btn_container)
# Disclaimer
layout.addSpacing(20)
layout.addSpacing(12) # Reduced from 20
disclaimer = QLabel(DISCLAIMER_TEXT)
disclaimer.setWordWrap(True)
disclaimer.setAlignment(Qt.AlignCenter)
@@ -104,6 +120,20 @@ class MainMenu(QWidget):
self.setLayout(layout)
def showEvent(self, event):
"""Called when the widget becomes visible - ensure minimum size only"""
super().showEvent(event)
try:
main_window = self.window()
if main_window:
from PySide6.QtCore import QSize
# Only set minimum size - DO NOT RESIZE
main_window.setMaximumSize(QSize(16777215, 16777215))
set_responsive_minimum(main_window, min_width=960, min_height=420)
# DO NOT resize - let window stay at current size
except Exception:
pass
def menu_action(self, action_id):
if action_id == "exit_jackify":
from PySide6.QtWidgets import QApplication

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ from PySide6.QtGui import QFont, QPalette, QColor, QPixmap
# Import our GUI services
from jackify.backend.models.configuration import SystemInfo
from ..shared_theme import JACKIFY_COLOR_BLUE
from ..utils import set_responsive_minimum
# Constants
DEBUG_BORDERS = False
@@ -77,8 +78,9 @@ class ModlistTasksScreen(QWidget):
"""Set up the user interface"""
main_layout = QVBoxLayout(self)
main_layout.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
main_layout.setContentsMargins(50, 50, 50, 50)
main_layout.setContentsMargins(30, 30, 30, 30) # Reduced from 50
main_layout.setSpacing(12) # Match main menu spacing
if self.debug:
self.setStyleSheet("border: 2px solid green;")
@@ -93,38 +95,43 @@ class ModlistTasksScreen(QWidget):
def _setup_header(self, layout):
"""Set up the header section"""
header_widget = QWidget()
header_layout = QVBoxLayout()
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(2)
# Title
title = QLabel("<b>Modlist Tasks</b>")
title.setStyleSheet(f"font-size: 24px; color: {JACKIFY_COLOR_BLUE};")
title.setStyleSheet(f"font-size: 20px; color: {JACKIFY_COLOR_BLUE};")
title.setAlignment(Qt.AlignHCenter)
header_layout.addWidget(title)
# Add a spacer to match main menu vertical spacing
header_layout.addSpacing(16)
# Description
header_layout.addSpacing(10)
# Description area with fixed height
desc = QLabel(
"Manage your modlists with native Linux tools. Choose "
"from the options below to install or configure modlists.<br>&nbsp;"
"from the options below to install or configure modlists."
)
desc.setWordWrap(True)
desc.setStyleSheet("color: #ccc;")
desc.setStyleSheet("color: #ccc; font-size: 13px;")
desc.setAlignment(Qt.AlignHCenter)
desc.setMaximumHeight(50) # Fixed height for description zone
header_layout.addWidget(desc)
header_layout.addSpacing(24)
header_layout.addSpacing(12)
# Separator
sep = QLabel()
sep.setFixedHeight(2)
sep.setStyleSheet("background: #fff;")
header_layout.addWidget(sep)
header_layout.addSpacing(16)
layout.addLayout(header_layout)
header_layout.addSpacing(10)
header_widget.setLayout(header_layout)
header_widget.setFixedHeight(120) # Fixed total header height
layout.addWidget(header_widget)
def _setup_menu_buttons(self, layout):
"""Set up the menu buttons section"""
@@ -140,12 +147,12 @@ class ModlistTasksScreen(QWidget):
# Create grid layout for buttons
button_grid = QGridLayout()
button_grid.setSpacing(16)
button_grid.setSpacing(12) # Reduced from 16
button_grid.setAlignment(Qt.AlignHCenter)
button_width = 400
button_height = 50
button_height = 40 # Reduced from 50
for i, (label, action_id, description) in enumerate(MENU_ITEMS):
# Create button
btn = QPushButton(label)
@@ -155,8 +162,8 @@ class ModlistTasksScreen(QWidget):
background-color: #4a5568;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
border-radius: 6px;
font-size: 13px;
font-weight: bold;
text-align: center;
}}
@@ -168,11 +175,11 @@ class ModlistTasksScreen(QWidget):
}}
""")
btn.clicked.connect(lambda checked, a=action_id: self.menu_action(a))
# Create description label
desc_label = QLabel(description)
desc_label.setAlignment(Qt.AlignHCenter)
desc_label.setStyleSheet("color: #999; font-size: 12px;")
desc_label.setStyleSheet("color: #999; font-size: 11px;") # Reduced from 12px
desc_label.setWordWrap(True)
desc_label.setFixedWidth(button_width)
@@ -208,7 +215,21 @@ class ModlistTasksScreen(QWidget):
"""Return to main menu"""
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(self.main_menu_index)
def showEvent(self, event):
"""Called when the widget becomes visible - resize to compact size"""
super().showEvent(event)
try:
main_window = self.window()
if main_window:
from PySide6.QtCore import QSize
# Only set minimum size - DO NOT RESIZE
main_window.setMaximumSize(QSize(16777215, 16777215))
set_responsive_minimum(main_window, min_width=960, min_height=420)
# DO NOT resize - let window stay at current size
except Exception:
pass
def cleanup(self):
"""Clean up resources when the screen is closed"""
pass