From 8661f8963e7d8ba53225543643b23c21db6b0b3d Mon Sep 17 00:00:00 2001 From: Omni Date: Sun, 28 Sep 2025 12:15:44 +0100 Subject: [PATCH] Sync from development - prepare for v0.1.5.1 --- CHANGELOG.md | 10 + jackify/__init__.py | 2 +- .../backend/handlers/winetricks_handler.py | 200 +++++++++++++++++- .../services/automated_prefix_service.py | 49 +++-- .../backend/services/native_steam_service.py | 108 +++++++++- jackify/frontends/gui/main.py | 7 +- jackify/frontends/gui/mixins/__init__.py | 10 + .../gui/mixins/operation_lock_mixin.py | 66 ++++++ .../gui/screens/configure_existing_modlist.py | 24 +++ .../gui/screens/configure_new_modlist.py | 21 ++ .../frontends/gui/screens/install_modlist.py | 35 ++- 11 files changed, 502 insertions(+), 30 deletions(-) create mode 100644 jackify/frontends/gui/mixins/__init__.py create mode 100644 jackify/frontends/gui/mixins/operation_lock_mixin.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 275d7d7..4eb9674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Jackify Changelog +## v0.1.5.1 - Bug Fixes +**Release Date:** September 28, 2025 + +### Bug Fixes +- Fixed Steam user detection in multi-user environments +- Fixed controls not re-enabling after workflow errors +- Fixed screen state persistence between workflows + +--- + ## v0.1.5 - Winetricks Integration & Enhanced Compatibility **Release Date:** September 26, 2025 diff --git a/jackify/__init__.py b/jackify/__init__.py index 08adcf0..30f3842 100644 --- a/jackify/__init__.py +++ b/jackify/__init__.py @@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing Wabbajack modlists natively on Linux systems. """ -__version__ = "0.1.5" +__version__ = "0.1.5.1" diff --git a/jackify/backend/handlers/winetricks_handler.py b/jackify/backend/handlers/winetricks_handler.py index 8757963..a41e61f 100644 --- a/jackify/backend/handlers/winetricks_handler.py +++ b/jackify/backend/handlers/winetricks_handler.py @@ -200,18 +200,26 @@ class WinetricksHandler: env['WINETRICKS_CACHE'] = str(jackify_cache_dir) if specific_components is not None: - components_to_install = specific_components - self.logger.info(f"Installing specific components: {components_to_install}") + all_components = specific_components + self.logger.info(f"Installing specific components: {all_components}") else: - components_to_install = ["fontsmooth=rgb", "xact", "xact_x64", "vcrun2022"] - self.logger.info(f"Installing default components: {components_to_install}") + all_components = ["fontsmooth=rgb", "xact", "xact_x64", "vcrun2022"] + self.logger.info(f"Installing default components: {all_components}") - if not components_to_install: + if not all_components: self.logger.info("No Wine components to install.") return True - self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Components: {components_to_install}") + # Reorder components for proper installation sequence + components_to_install = self._reorder_components_for_installation(all_components) + self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}") + # Install components separately if dotnet40 is present (mimics protontricks behavior) + if "dotnet40" in components_to_install: + self.logger.info("dotnet40 detected - installing components separately like protontricks") + return self._install_components_separately(components_to_install, wineprefix, wine_binary, env) + + # For non-dotnet40 installations, install all components together (faster) max_attempts = 3 for attempt in range(1, max_attempts + 1): if attempt > 1: @@ -240,6 +248,23 @@ class WinetricksHandler: self.logger.info("Wine Component installation command completed successfully.") return True else: + # Special handling for dotnet40 verification issue (mimics protontricks behavior) + if "dotnet40" in components_to_install and "ngen.exe not found" in result.stderr: + self.logger.warning("dotnet40 verification warning (common in Steam Proton prefixes)") + self.logger.info("Checking if dotnet40 was actually installed...") + + # Check if dotnet40 appears in winetricks.log (indicates successful installation) + log_path = os.path.join(wineprefix, 'winetricks.log') + if os.path.exists(log_path): + try: + with open(log_path, 'r') as f: + log_content = f.read() + if 'dotnet40' in log_content: + self.logger.info("dotnet40 found in winetricks.log - installation succeeded despite verification warning") + return True + except Exception as e: + self.logger.warning(f"Could not read winetricks.log: {e}") + self.logger.error(f"Winetricks command failed (Attempt {attempt}/{max_attempts}). Return Code: {result.returncode}") self.logger.error(f"Stdout: {result.stdout.strip()}") self.logger.error(f"Stderr: {result.stderr.strip()}") @@ -250,6 +275,169 @@ class WinetricksHandler: self.logger.error(f"Failed to install Wine components after {max_attempts} attempts.") return False + def _reorder_components_for_installation(self, components: list) -> list: + """ + Reorder components for proper installation sequence. + Critical: dotnet40 must be installed before dotnet6/dotnet7 to avoid conflicts. + """ + # Simple reordering: dotnet40 first, then everything else + reordered = [] + + # Add dotnet40 first if it exists + if "dotnet40" in components: + reordered.append("dotnet40") + + # Add all other components in original order + for component in components: + if component != "dotnet40": + reordered.append(component) + + if reordered != components: + self.logger.info(f"Reordered for dotnet40 compatibility: {reordered}") + + return reordered + + def _prepare_prefix_for_dotnet(self, wineprefix: str, wine_binary: str) -> bool: + """ + Prepare the Wine prefix for .NET installation by mimicking protontricks preprocessing. + This removes mono components and specific symlinks that interfere with .NET installation. + """ + try: + env = os.environ.copy() + env['WINEDEBUG'] = '-all' + env['WINEPREFIX'] = wineprefix + + # Step 1: Remove mono components (mimics protontricks behavior) + self.logger.info("Preparing prefix for .NET installation: removing mono") + mono_result = subprocess.run([ + self.winetricks_path, + '-q', + 'remove_mono' + ], env=env, capture_output=True, text=True, timeout=300) + + if mono_result.returncode != 0: + self.logger.warning(f"Mono removal warning (non-critical): {mono_result.stderr}") + + # Step 2: Set Windows version to XP (protontricks uses winxp for dotnet40) + self.logger.info("Setting Windows version to XP for .NET compatibility") + winxp_result = subprocess.run([ + self.winetricks_path, + '-q', + 'winxp' + ], env=env, capture_output=True, text=True, timeout=300) + + if winxp_result.returncode != 0: + self.logger.warning(f"Windows XP setting warning: {winxp_result.stderr}") + + # Step 3: Remove mscoree.dll symlinks (critical for .NET installation) + self.logger.info("Removing problematic mscoree.dll symlinks") + dosdevices_path = os.path.join(wineprefix, 'dosdevices', 'c:') + mscoree_paths = [ + os.path.join(dosdevices_path, 'windows', 'syswow64', 'mscoree.dll'), + os.path.join(dosdevices_path, 'windows', 'system32', 'mscoree.dll') + ] + + for dll_path in mscoree_paths: + if os.path.exists(dll_path) or os.path.islink(dll_path): + try: + os.remove(dll_path) + self.logger.debug(f"Removed symlink: {dll_path}") + except Exception as e: + self.logger.warning(f"Could not remove {dll_path}: {e}") + + self.logger.info("Prefix preparation complete for .NET installation") + return True + + except Exception as e: + self.logger.error(f"Error preparing prefix for .NET: {e}") + return False + + def _install_components_separately(self, components: list, wineprefix: str, wine_binary: str, base_env: dict) -> bool: + """ + Install components separately like protontricks does. + This is necessary when dotnet40 is present to avoid component conflicts. + """ + self.logger.info(f"Installing {len(components)} components separately (protontricks style)") + + for i, component in enumerate(components, 1): + self.logger.info(f"Installing component {i}/{len(components)}: {component}") + + # Prepare environment for this component + env = base_env.copy() + + # Special preprocessing for dotnet40 only + if component == "dotnet40": + self.logger.info("Applying dotnet40 preprocessing") + if not self._prepare_prefix_for_dotnet(wineprefix, wine_binary): + self.logger.error("Failed to prepare prefix for dotnet40") + return False + else: + # For non-dotnet40 components, ensure we're in Windows 10 mode + self.logger.debug(f"Installing {component} in standard mode") + try: + subprocess.run([ + self.winetricks_path, '-q', 'win10' + ], env=env, capture_output=True, text=True, timeout=300) + except Exception as e: + self.logger.warning(f"Could not set win10 mode for {component}: {e}") + + # Install this component + max_attempts = 3 + component_success = False + + for attempt in range(1, max_attempts + 1): + if attempt > 1: + self.logger.warning(f"Retrying {component} installation (attempt {attempt}/{max_attempts})") + self._cleanup_wine_processes() + + try: + cmd = [self.winetricks_path, '--unattended', component] + env['WINEPREFIX'] = wineprefix + env['WINE'] = wine_binary + + self.logger.debug(f"Running: {' '.join(cmd)}") + + result = subprocess.run( + cmd, + env=env, + capture_output=True, + text=True, + timeout=600 + ) + + if result.returncode == 0: + self.logger.info(f"✓ {component} installed successfully") + component_success = True + break + else: + # Special handling for dotnet40 verification issue + if component == "dotnet40" and "ngen.exe not found" in result.stderr: + self.logger.warning("dotnet40 verification warning (expected in Steam Proton)") + + # Check winetricks.log for actual success + log_path = os.path.join(wineprefix, 'winetricks.log') + if os.path.exists(log_path): + try: + with open(log_path, 'r') as f: + if 'dotnet40' in f.read(): + self.logger.info("✓ dotnet40 confirmed in winetricks.log") + component_success = True + break + except Exception as e: + self.logger.warning(f"Could not read winetricks.log: {e}") + + self.logger.error(f"✗ {component} failed (attempt {attempt}): {result.stderr.strip()[:200]}") + + except Exception as e: + self.logger.error(f"Error installing {component} (attempt {attempt}): {e}") + + if not component_success: + self.logger.error(f"Failed to install {component} after {max_attempts} attempts") + return False + + self.logger.info("✓ All components installed successfully using separate sessions") + return True + def _cleanup_wine_processes(self): """ Internal method to clean up wine processes during component installation diff --git a/jackify/backend/services/automated_prefix_service.py b/jackify/backend/services/automated_prefix_service.py index ba43f8d..d30ffb5 100644 --- a/jackify/backend/services/automated_prefix_service.py +++ b/jackify/backend/services/automated_prefix_service.py @@ -333,15 +333,18 @@ class AutomatedPrefixService: logger.error(f"Steam userdata directory not found: {userdata_dir}") return None - # Find user directories (excluding user 0 which is a system account) - user_dirs = [d for d in userdata_dir.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"] - if not user_dirs: - logger.error("No valid Steam user directories found in userdata (user 0 is not valid)") + # Use NativeSteamService for proper user detection + from ..services.native_steam_service import NativeSteamService + steam_service = NativeSteamService() + + if not steam_service.find_steam_user(): + logger.error("Could not detect Steam user for shortcuts") + return None + + shortcuts_path = steam_service.get_shortcuts_vdf_path() + if not shortcuts_path: + logger.error("Could not get shortcuts.vdf path from Steam service") return None - - # Use the first valid user directory found - user_dir = user_dirs[0] - shortcuts_path = user_dir / "config" / "shortcuts.vdf" logger.debug(f"Looking for shortcuts.vdf at: {shortcuts_path}") if not shortcuts_path.exists(): @@ -2527,15 +2530,31 @@ echo Prefix creation complete. Returns: Path to localconfig.vdf or None if not found """ - # Try the standard Steam userdata path - steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata" - if steam_userdata_path.exists(): - # Find user directories (excluding user 0 which is a system account) - user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"] - if user_dirs: - localconfig_path = user_dirs[0] / "config" / "localconfig.vdf" + # Use NativeSteamService for proper user detection + try: + from ..services.native_steam_service import NativeSteamService + steam_service = NativeSteamService() + + if steam_service.find_steam_user(): + localconfig_path = steam_service.user_config_path / "localconfig.vdf" if localconfig_path.exists(): return str(localconfig_path) + except Exception as e: + logger.error(f"Error using Steam service for localconfig.vdf detection: {e}") + + # Fallback to manual detection + steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata" + if steam_userdata_path.exists(): + user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"] + if user_dirs: + # Use most recently modified directory as fallback + try: + most_recent = max(user_dirs, key=lambda d: d.stat().st_mtime) + localconfig_path = most_recent / "config" / "localconfig.vdf" + if localconfig_path.exists(): + return str(localconfig_path) + except Exception: + pass logger.error("Could not find localconfig.vdf") return None diff --git a/jackify/backend/services/native_steam_service.py b/jackify/backend/services/native_steam_service.py index eddb44d..82634d8 100644 --- a/jackify/backend/services/native_steam_service.py +++ b/jackify/backend/services/native_steam_service.py @@ -15,6 +15,8 @@ import vdf from pathlib import Path from typing import Optional, Tuple, Dict, Any, List +from ..handlers.vdf_handler import VDFHandler + logger = logging.getLogger(__name__) class NativeSteamService: @@ -39,18 +41,22 @@ class NativeSteamService: if not self.userdata_path.exists(): logger.error("Steam userdata directory not found") return False - + # Find user directories (excluding user 0 which is a system account) user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"] if not user_dirs: logger.error("No valid Steam user directories found (user 0 is not valid for shortcuts)") return False - - # Use the first valid user directory - user_dir = user_dirs[0] + + # Detect the correct Steam user + user_dir = self._detect_active_steam_user(user_dirs) + if not user_dir: + logger.error("Could not determine active Steam user") + return False + self.user_id = user_dir.name self.user_config_path = user_dir / "config" - + logger.info(f"Found Steam user: {self.user_id}") logger.info(f"User config path: {self.user_config_path}") return True @@ -58,7 +64,97 @@ class NativeSteamService: except Exception as e: logger.error(f"Error finding Steam user: {e}") return False - + + def _detect_active_steam_user(self, user_dirs: List[Path]) -> Optional[Path]: + """ + Detect the active Steam user from available user directories. + + Priority: + 1. Single non-0 user: Use automatically + 2. Multiple users: Parse loginusers.vdf to find logged-in user + 3. Fallback: Most recently active user directory + + Args: + user_dirs: List of valid user directories + + Returns: + Path to the active user directory, or None if detection fails + """ + if len(user_dirs) == 1: + logger.info(f"Single Steam user found: {user_dirs[0].name}") + return user_dirs[0] + + logger.info(f"Multiple Steam users found: {[d.name for d in user_dirs]}") + + # Try to parse loginusers.vdf to find logged-in user + loginusers_path = self.steam_path / "loginusers.vdf" + active_user = self._parse_loginusers_vdf(loginusers_path) + + if active_user: + # Find matching user directory + for user_dir in user_dirs: + if user_dir.name == active_user: + logger.info(f"Found logged-in Steam user from loginusers.vdf: {active_user}") + return user_dir + + logger.warning(f"Logged-in user {active_user} from loginusers.vdf not found in user directories") + + # Fallback: Use most recently modified user directory + try: + most_recent = max(user_dirs, key=lambda d: d.stat().st_mtime) + logger.info(f"Using most recently active Steam user directory: {most_recent.name}") + return most_recent + except Exception as e: + logger.error(f"Error determining most recent user directory: {e}") + # Final fallback: Use first user + logger.warning(f"Using first user directory as final fallback: {user_dirs[0].name}") + return user_dirs[0] + + def _parse_loginusers_vdf(self, loginusers_path: Path) -> Optional[str]: + """ + Parse loginusers.vdf to find the currently logged-in Steam user. + + Args: + loginusers_path: Path to loginusers.vdf + + Returns: + Steam user ID as string, or None if parsing fails + """ + try: + if not loginusers_path.exists(): + logger.debug(f"loginusers.vdf not found at {loginusers_path}") + return None + + # Load VDF data + vdf_data = VDFHandler.load(str(loginusers_path), binary=False) + if not vdf_data: + logger.error("Failed to parse loginusers.vdf") + return None + + users_section = vdf_data.get("users", {}) + if not users_section: + logger.debug("No users section found in loginusers.vdf") + return None + + # Find user marked as logged in + for user_id, user_data in users_section.items(): + if isinstance(user_data, dict): + # Check for indicators of logged-in status + if user_data.get("MostRecent") == "1" or user_data.get("WantsOfflineMode") == "0": + logger.debug(f"Found most recent/logged-in user: {user_id}") + return user_id + + # If no specific user found, try to get the first valid user + if users_section: + first_user = next(iter(users_section.keys())) + logger.debug(f"No specific logged-in user found, using first user: {first_user}") + return first_user + + except Exception as e: + logger.error(f"Error parsing loginusers.vdf: {e}") + + return None + def get_shortcuts_vdf_path(self) -> Optional[Path]: """Get the path to shortcuts.vdf""" if not self.user_config_path: diff --git a/jackify/frontends/gui/main.py b/jackify/frontends/gui/main.py index a320f7c..a894061 100644 --- a/jackify/frontends/gui/main.py +++ b/jackify/frontends/gui/main.py @@ -884,7 +884,12 @@ class JackifyMainWindow(QMainWindow): self._check_protontricks_on_startup() def _debug_screen_change(self, index): - """Debug method to track screen changes""" + """Handle screen changes - debug logging and state reset""" + # Reset screen state when switching to workflow screens + widget = self.stacked_widget.widget(index) + if widget and hasattr(widget, 'reset_screen_to_defaults'): + widget.reset_screen_to_defaults() + # Only show debug info if debug mode is enabled from jackify.backend.handlers.config_handler import ConfigHandler config_handler = ConfigHandler() diff --git a/jackify/frontends/gui/mixins/__init__.py b/jackify/frontends/gui/mixins/__init__.py new file mode 100644 index 0000000..86167ca --- /dev/null +++ b/jackify/frontends/gui/mixins/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GUI Mixins Package +Reusable mixins for GUI functionality +""" + +from .operation_lock_mixin import OperationLockMixin + +__all__ = ['OperationLockMixin'] \ No newline at end of file diff --git a/jackify/frontends/gui/mixins/operation_lock_mixin.py b/jackify/frontends/gui/mixins/operation_lock_mixin.py new file mode 100644 index 0000000..c7db77e --- /dev/null +++ b/jackify/frontends/gui/mixins/operation_lock_mixin.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Operation Lock Mixin +Provides reliable button state management for GUI operations +""" + +from contextlib import contextmanager + + +class OperationLockMixin: + """ + Mixin that provides reliable button state management. + Ensures controls are always re-enabled after operations, even if exceptions occur. + """ + + def operation_lock(self): + """ + Context manager that ensures controls are always re-enabled after operations. + + Usage: + with self.operation_lock(): + # Perform operation that might fail + risky_operation() + # Controls are guaranteed to be re-enabled here + """ + @contextmanager + def lock_manager(): + try: + if hasattr(self, '_disable_controls_during_operation'): + self._disable_controls_during_operation() + yield + finally: + # Ensure controls are re-enabled even if exceptions occur + if hasattr(self, '_enable_controls_after_operation'): + self._enable_controls_after_operation() + + return lock_manager() + + def safe_operation(self, operation_func, *args, **kwargs): + """ + Execute an operation with automatic button state management. + + Args: + operation_func: Function to execute + *args, **kwargs: Arguments to pass to operation_func + + Returns: + Result of operation_func or None if exception occurred + """ + try: + with self.operation_lock(): + return operation_func(*args, **kwargs) + except Exception as e: + # Log the error but don't re-raise - controls are already re-enabled + if hasattr(self, 'logger'): + self.logger.error(f"Operation failed: {e}", exc_info=True) + # Could also show user error dialog here if needed + return None + + def reset_screen_to_defaults(self): + """ + Reset the screen to default state when navigating back from main menu. + Override this method in subclasses to implement screen-specific reset logic. + """ + pass # Default implementation does nothing - subclasses should override \ No newline at end of file diff --git a/jackify/frontends/gui/screens/configure_existing_modlist.py b/jackify/frontends/gui/screens/configure_existing_modlist.py index e342864..6a1132b 100644 --- a/jackify/frontends/gui/screens/configure_existing_modlist.py +++ b/jackify/frontends/gui/screens/configure_existing_modlist.py @@ -738,6 +738,30 @@ class ConfigureExistingModlistScreen(QWidget): else: return f"{elapsed_seconds_remainder} seconds" + def reset_screen_to_defaults(self): + """Reset the screen to default state when navigating back from main menu""" + # Clear the shortcut selection + self.shortcut_combo.clear() + self.shortcut_map.clear() + # Auto-refresh modlist list when screen is entered + self.refresh_modlist_list() + + # Clear console and process monitor + self.console.clear() + self.process_monitor.clear() + + # Reset resolution combo to saved config preference + saved_resolution = self.resolution_service.get_saved_resolution() + if saved_resolution: + combo_items = [self.resolution_combo.itemText(i) for i in range(self.resolution_combo.count())] + resolution_index = self.resolution_service.get_resolution_index(saved_resolution, combo_items) + self.resolution_combo.setCurrentIndex(resolution_index) + elif self.resolution_combo.count() > 0: + self.resolution_combo.setCurrentIndex(0) # Fallback to "Leave unchanged" + + # Re-enable controls (in case they were disabled from previous errors) + self._enable_controls_after_operation() + def cleanup(self): """Clean up any running threads when the screen is closed""" debug_print("DEBUG: cleanup called - cleaning up ConfigurationThread") diff --git a/jackify/frontends/gui/screens/configure_new_modlist.py b/jackify/frontends/gui/screens/configure_new_modlist.py index 0456c49..b5e521b 100644 --- a/jackify/frontends/gui/screens/configure_new_modlist.py +++ b/jackify/frontends/gui/screens/configure_new_modlist.py @@ -1326,6 +1326,27 @@ class ConfigureNewModlistScreen(QWidget): btn_exit.clicked.connect(on_exit) dlg.exec() + def reset_screen_to_defaults(self): + """Reset the screen to default state when navigating back from main menu""" + # Reset form fields + self.install_dir_edit.setText("/path/to/Modlist/ModOrganizer.exe") + + # Clear console and process monitor + self.console.clear() + self.process_monitor.clear() + + # Reset resolution combo to saved config preference + saved_resolution = self.resolution_service.get_saved_resolution() + if saved_resolution: + combo_items = [self.resolution_combo.itemText(i) for i in range(self.resolution_combo.count())] + resolution_index = self.resolution_service.get_resolution_index(saved_resolution, combo_items) + self.resolution_combo.setCurrentIndex(resolution_index) + elif self.resolution_combo.count() > 0: + self.resolution_combo.setCurrentIndex(0) # Fallback to "Leave unchanged" + + # Re-enable controls (in case they were disabled from previous errors) + self._enable_controls_after_operation() + def cleanup(self): """Clean up any running threads when the screen is closed""" debug_print("DEBUG: cleanup called - cleaning up threads") diff --git a/jackify/frontends/gui/screens/install_modlist.py b/jackify/frontends/gui/screens/install_modlist.py index 3fa8d68..b6a000a 100644 --- a/jackify/frontends/gui/screens/install_modlist.py +++ b/jackify/frontends/gui/screens/install_modlist.py @@ -2023,7 +2023,10 @@ class InstallModlistScreen(QWidget): """Handle configuration error on main thread""" self._safe_append_text(f"Configuration failed with error: {error_message}") MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}") - + + # Re-enable all controls on error + self._enable_controls_after_operation() + # 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 @@ -2676,6 +2679,36 @@ https://wiki.scenicroute.games/Somnium/1_Installation.html""" self.cleanup_processes() self.go_back() + def reset_screen_to_defaults(self): + """Reset the screen to default state when navigating back from main menu""" + # Reset form fields + self.modlist_btn.setText("Select Modlist") + self.modlist_btn.setEnabled(False) + self.file_edit.setText("") + self.modlist_name_edit.setText("") + self.install_dir_edit.setText(self.config_handler.get_modlist_install_base_dir()) + # Reset game type button + self.game_type_btn.setText("Please Select...") + + # Clear console and process monitor + self.console.clear() + self.process_monitor.clear() + + # Reset tabs to first tab (Online) + self.source_tabs.setCurrentIndex(0) + + # Reset resolution combo to saved config preference + saved_resolution = self.resolution_service.get_saved_resolution() + if saved_resolution: + combo_items = [self.resolution_combo.itemText(i) for i in range(self.resolution_combo.count())] + resolution_index = self.resolution_service.get_resolution_index(saved_resolution, combo_items) + self.resolution_combo.setCurrentIndex(resolution_index) + elif self.resolution_combo.count() > 0: + self.resolution_combo.setCurrentIndex(0) # Fallback to "Leave unchanged" + + # Re-enable controls (in case they were disabled from previous errors) + self._enable_controls_after_operation() + def closeEvent(self, event): """Handle window close event - clean up processes""" self.cleanup_processes()