mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Sync from development - prepare for v0.2.0.9
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,5 +1,23 @@
|
|||||||
# Jackify Changelog
|
# Jackify Changelog
|
||||||
|
|
||||||
|
## v0.2.0.9 - Critical Configuration Fixes
|
||||||
|
**Release Date:** 2025-12-31
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fixed AppID conversion bug causing Configure Existing failures
|
||||||
|
- Fixed missing MessageService import crash in Configure Existing
|
||||||
|
- Fixed RecursionError in config_handler.py logger
|
||||||
|
- Fixed winetricks automatic fallback to protontricks (was silently failing)
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- Added detailed progress indicators for configuration workflows
|
||||||
|
- Fixed progress bar completion showing 100% instead of 95%
|
||||||
|
- Removed debug logging noise from file progress widget
|
||||||
|
- Enhanced Premium detection diagnostics for Issue #111
|
||||||
|
- Flatpak protontricks now auto-granted cache access for faster subsequent installs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.2.0.8 - Bug Fixes and Improvements
|
## v0.2.0.8 - Bug Fixes and Improvements
|
||||||
**Release Date:** 2025-12-29
|
**Release Date:** 2025-12-29
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
|
|||||||
Wabbajack modlists natively on Linux systems.
|
Wabbajack modlists natively on Linux systems.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.2.0.8"
|
__version__ = "0.2.0.9"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Handles application settings and configuration
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
@@ -211,7 +212,8 @@ class ConfigHandler:
|
|||||||
config.update(saved_config)
|
config.update(saved_config)
|
||||||
return config
|
return config
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error reading configuration from disk: {e}")
|
# Don't use logger here - can cause recursion if logger tries to access config
|
||||||
|
print(f"Warning: Error reading configuration from disk: {e}", file=sys.stderr)
|
||||||
return self.settings.copy()
|
return self.settings.copy()
|
||||||
|
|
||||||
def reload_config(self):
|
def reload_config(self):
|
||||||
|
|||||||
@@ -571,15 +571,19 @@ class ModlistMenuHandler:
|
|||||||
self.logger.warning(f"[DEBUG] Could not find AppID for {context['name']} with exe {context['mo2_exe_path']}")
|
self.logger.warning(f"[DEBUG] Could not find AppID for {context['name']} with exe {context['mo2_exe_path']}")
|
||||||
set_modlist_result = self.modlist_handler.set_modlist(context)
|
set_modlist_result = self.modlist_handler.set_modlist(context)
|
||||||
self.logger.debug(f"[DEBUG] set_modlist returned: {set_modlist_result}")
|
self.logger.debug(f"[DEBUG] set_modlist returned: {set_modlist_result}")
|
||||||
|
|
||||||
|
# Check GUI mode early to avoid input() calls in GUI context
|
||||||
|
import os
|
||||||
|
gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
|
||||||
|
|
||||||
if not set_modlist_result:
|
if not set_modlist_result:
|
||||||
print(f"{COLOR_ERROR}\nError setting up context for configuration.{COLOR_RESET}")
|
print(f"{COLOR_ERROR}\nError setting up context for configuration.{COLOR_RESET}")
|
||||||
self.logger.error(f"set_modlist failed for {context.get('name')}")
|
self.logger.error(f"set_modlist failed for {context.get('name')}")
|
||||||
input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")
|
if not gui_mode:
|
||||||
|
input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# --- Resolution selection logic for GUI mode ---
|
# --- Resolution selection logic for GUI mode ---
|
||||||
import os
|
|
||||||
gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
|
|
||||||
selected_resolution = context.get('resolution', None)
|
selected_resolution = context.get('resolution', None)
|
||||||
if gui_mode:
|
if gui_mode:
|
||||||
# If resolution is provided, set it and do not prompt
|
# If resolution is provided, set it and do not prompt
|
||||||
@@ -654,7 +658,8 @@ class ModlistMenuHandler:
|
|||||||
print("NOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of")
|
print("NOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of")
|
||||||
print(" Valve's Proton 10 (known ENB compatibility issues in Valve's Proton 10).")
|
print(" Valve's Proton 10 (known ENB compatibility issues in Valve's Proton 10).")
|
||||||
print("")
|
print("")
|
||||||
print("Detailed log available at: ~/Jackify/logs/Configure_New_Modlist_workflow.log")
|
from jackify.shared.paths import get_jackify_logs_dir
|
||||||
|
print(f"Detailed log available at: {get_jackify_logs_dir()}/Configure_New_Modlist_workflow.log")
|
||||||
# Only wait for input in CLI mode, not GUI mode
|
# Only wait for input in CLI mode, not GUI mode
|
||||||
if not gui_mode:
|
if not gui_mode:
|
||||||
input(f"{COLOR_PROMPT}Press Enter to return to the menu...{COLOR_RESET}")
|
input(f"{COLOR_PROMPT}Press Enter to return to the menu...{COLOR_RESET}")
|
||||||
|
|||||||
@@ -777,6 +777,56 @@ class ProtontricksHandler:
|
|||||||
self.logger.error(f"Error running protontricks-launch: {e}")
|
self.logger.error(f"Error running protontricks-launch: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _ensure_flatpak_cache_access(self, cache_path: Path) -> bool:
|
||||||
|
"""
|
||||||
|
Ensure flatpak protontricks has filesystem access to the winetricks cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cache_path: Path to winetricks cache directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if access granted or already exists, False on failure
|
||||||
|
"""
|
||||||
|
if self.which_protontricks != 'flatpak':
|
||||||
|
return True # Not flatpak, no action needed
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if flatpak already has access to this path
|
||||||
|
result = subprocess.run(
|
||||||
|
['flatpak', 'override', '--user', '--show', 'com.github.Matoking.protontricks'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Check if cache path is already in filesystem overrides
|
||||||
|
cache_str = str(cache_path.resolve())
|
||||||
|
if f'filesystems=' in result.stdout and cache_str in result.stdout:
|
||||||
|
self.logger.debug(f"Flatpak protontricks already has access to cache: {cache_str}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Grant access to cache directory
|
||||||
|
self.logger.info(f"Granting flatpak protontricks access to winetricks cache: {cache_path}")
|
||||||
|
result = subprocess.run(
|
||||||
|
['flatpak', 'override', '--user', 'com.github.Matoking.protontricks',
|
||||||
|
f'--filesystem={cache_path.resolve()}'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.logger.info("Successfully granted flatpak protontricks cache access")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Failed to grant flatpak cache access: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Could not configure flatpak cache access: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def install_wine_components(self, appid, game_var, specific_components: Optional[List[str]] = None):
|
def install_wine_components(self, appid, game_var, specific_components: Optional[List[str]] = None):
|
||||||
"""
|
"""
|
||||||
Install the specified Wine components into the given prefix using protontricks.
|
Install the specified Wine components into the given prefix using protontricks.
|
||||||
@@ -820,6 +870,10 @@ class ProtontricksHandler:
|
|||||||
from jackify.shared.paths import get_jackify_data_dir
|
from jackify.shared.paths import get_jackify_data_dir
|
||||||
jackify_cache_dir = get_jackify_data_dir() / 'winetricks_cache'
|
jackify_cache_dir = get_jackify_data_dir() / 'winetricks_cache'
|
||||||
jackify_cache_dir.mkdir(parents=True, exist_ok=True)
|
jackify_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Ensure flatpak protontricks has access to cache (no-op for native)
|
||||||
|
self._ensure_flatpak_cache_access(jackify_cache_dir)
|
||||||
|
|
||||||
env['WINETRICKS_CACHE'] = str(jackify_cache_dir)
|
env['WINETRICKS_CACHE'] = str(jackify_cache_dir)
|
||||||
self.logger.info(f"Using winetricks cache: {jackify_cache_dir}")
|
self.logger.info(f"Using winetricks cache: {jackify_cache_dir}")
|
||||||
if specific_components is not None:
|
if specific_components is not None:
|
||||||
|
|||||||
@@ -955,7 +955,10 @@ class ShortcutHandler:
|
|||||||
|
|
||||||
def get_appid_for_shortcut(self, shortcut_name: str, exe_path: Optional[str] = None) -> Optional[str]:
|
def get_appid_for_shortcut(self, shortcut_name: str, exe_path: Optional[str] = None) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Find the current AppID for a given shortcut name and (optionally) executable path using protontricks.
|
Find the current AppID for a given shortcut name and (optionally) executable path.
|
||||||
|
|
||||||
|
Primary method: Read directly from shortcuts.vdf (reliable, no external dependencies)
|
||||||
|
Fallback method: Use protontricks (if available)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
shortcut_name (str): The name of the Steam shortcut.
|
shortcut_name (str): The name of the Steam shortcut.
|
||||||
@@ -965,15 +968,22 @@ class ShortcutHandler:
|
|||||||
Optional[str]: The found AppID string, or None if not found or error occurs.
|
Optional[str]: The found AppID string, or None if not found or error occurs.
|
||||||
"""
|
"""
|
||||||
self.logger.info(f"Attempting to find current AppID for shortcut: '{shortcut_name}' (exe_path: '{exe_path}')")
|
self.logger.info(f"Attempting to find current AppID for shortcut: '{shortcut_name}' (exe_path: '{exe_path}')")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .protontricks_handler import ProtontricksHandler # Local import
|
appid = self.get_appid_from_vdf(shortcut_name, exe_path)
|
||||||
|
if appid:
|
||||||
|
self.logger.info(f"Successfully found AppID {appid} from shortcuts.vdf")
|
||||||
|
return appid
|
||||||
|
|
||||||
|
self.logger.info("AppID not found in shortcuts.vdf, trying protontricks as fallback...")
|
||||||
|
from .protontricks_handler import ProtontricksHandler
|
||||||
pt_handler = ProtontricksHandler(self.steamdeck)
|
pt_handler = ProtontricksHandler(self.steamdeck)
|
||||||
if not pt_handler.detect_protontricks():
|
if not pt_handler.detect_protontricks():
|
||||||
self.logger.error("Protontricks not detected")
|
self.logger.warning("Protontricks not detected - cannot use as fallback")
|
||||||
return None
|
return None
|
||||||
result = pt_handler.run_protontricks("-l")
|
result = pt_handler.run_protontricks("-l")
|
||||||
if not result or result.returncode != 0:
|
if not result or result.returncode != 0:
|
||||||
self.logger.error(f"Protontricks failed to list applications: {result.stderr if result else 'No result'}")
|
self.logger.warning(f"Protontricks fallback failed: {result.stderr if result else 'No result'}")
|
||||||
return None
|
return None
|
||||||
# Build a list of all shortcuts
|
# Build a list of all shortcuts
|
||||||
found_shortcuts = []
|
found_shortcuts = []
|
||||||
@@ -1022,6 +1032,62 @@ class ShortcutHandler:
|
|||||||
self.logger.exception("Traceback:")
|
self.logger.exception("Traceback:")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_appid_from_vdf(self, shortcut_name: str, exe_path: Optional[str] = None) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get AppID directly from shortcuts.vdf by reading the file and matching shortcut name/exe.
|
||||||
|
This is more reliable than using protontricks since it doesn't depend on external tools.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shortcut_name (str): The name of the Steam shortcut.
|
||||||
|
exe_path (Optional[str]): The path to the executable for additional validation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: The AppID as a string, or None if not found.
|
||||||
|
"""
|
||||||
|
self.logger.info(f"Looking up AppID from shortcuts.vdf for shortcut: '{shortcut_name}' (exe: '{exe_path}')")
|
||||||
|
|
||||||
|
if not self.shortcuts_path or not os.path.isfile(self.shortcuts_path):
|
||||||
|
self.logger.warning(f"Shortcuts.vdf not found at {self.shortcuts_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
shortcuts_data = VDFHandler.load(self.shortcuts_path, binary=True)
|
||||||
|
if not shortcuts_data or 'shortcuts' not in shortcuts_data:
|
||||||
|
self.logger.warning("No shortcuts found in shortcuts.vdf")
|
||||||
|
return None
|
||||||
|
|
||||||
|
shortcut_name_clean = shortcut_name.strip().lower()
|
||||||
|
|
||||||
|
for idx, shortcut in shortcuts_data['shortcuts'].items():
|
||||||
|
name = shortcut.get('AppName', shortcut.get('appname', '')).strip()
|
||||||
|
|
||||||
|
if name.lower() == shortcut_name_clean:
|
||||||
|
appid = shortcut.get('appid')
|
||||||
|
|
||||||
|
if appid:
|
||||||
|
if exe_path:
|
||||||
|
vdf_exe = shortcut.get('Exe', shortcut.get('exe', '')).strip('"').strip()
|
||||||
|
exe_path_norm = os.path.abspath(os.path.expanduser(exe_path)).lower()
|
||||||
|
vdf_exe_norm = os.path.abspath(os.path.expanduser(vdf_exe)).lower()
|
||||||
|
|
||||||
|
if vdf_exe_norm == exe_path_norm:
|
||||||
|
self.logger.info(f"Found AppID {appid} for shortcut '{name}' with matching exe '{vdf_exe}'")
|
||||||
|
return str(int(appid) & 0xFFFFFFFF)
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"Found shortcut '{name}' but exe doesn't match: '{vdf_exe}' vs '{exe_path}'")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.logger.info(f"Found AppID {appid} for shortcut '{name}' (no exe validation)")
|
||||||
|
return str(int(appid) & 0xFFFFFFFF)
|
||||||
|
|
||||||
|
self.logger.warning(f"No matching shortcut found in shortcuts.vdf for '{shortcut_name}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error reading shortcuts.vdf: {e}")
|
||||||
|
self.logger.exception("Traceback:")
|
||||||
|
return None
|
||||||
|
|
||||||
# --- Discovery Methods Moved from ModlistHandler ---
|
# --- Discovery Methods Moved from ModlistHandler ---
|
||||||
|
|
||||||
def _scan_shortcuts_for_executable(self, executable_name: str) -> List[Dict[str, str]]:
|
def _scan_shortcuts_for_executable(self, executable_name: str) -> List[Dict[str, str]]:
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ class WinetricksHandler:
|
|||||||
from .protontricks_handler import ProtontricksHandler
|
from .protontricks_handler import ProtontricksHandler
|
||||||
steamdeck = os.path.exists('/home/deck')
|
steamdeck = os.path.exists('/home/deck')
|
||||||
protontricks_handler = ProtontricksHandler(steamdeck)
|
protontricks_handler = ProtontricksHandler(steamdeck)
|
||||||
protontricks_available = protontricks_handler.is_available()
|
protontricks_available = protontricks_handler.detect_protontricks()
|
||||||
|
|
||||||
if protontricks_available:
|
if protontricks_available:
|
||||||
self.logger.warning("=" * 80)
|
self.logger.warning("=" * 80)
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ exit"""
|
|||||||
def detect_actual_prefix_appid(self, initial_appid: int, shortcut_name: str) -> Optional[int]:
|
def detect_actual_prefix_appid(self, initial_appid: int, shortcut_name: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
After Steam restart, detect the actual prefix AppID that was created.
|
After Steam restart, detect the actual prefix AppID that was created.
|
||||||
Use protontricks -l to find the actual positive AppID.
|
Uses direct VDF file reading to find the actual AppID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
initial_appid: The initial (negative) AppID from shortcuts.vdf
|
initial_appid: The initial (negative) AppID from shortcuts.vdf
|
||||||
@@ -503,42 +503,42 @@ exit"""
|
|||||||
The actual (positive) AppID of the created prefix, or None if not found
|
The actual (positive) AppID of the created prefix, or None if not found
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"Using protontricks -l to detect actual AppID for shortcut: {shortcut_name}")
|
logger.info(f"Using VDF to detect actual AppID for shortcut: {shortcut_name}")
|
||||||
|
|
||||||
# Wait up to 30 seconds for the shortcut to appear in protontricks
|
# Wait up to 30 seconds for Steam to process the shortcut
|
||||||
for i in range(30):
|
for i in range(30):
|
||||||
try:
|
try:
|
||||||
# Use the existing protontricks handler
|
from ..handlers.shortcut_handler import ShortcutHandler
|
||||||
from jackify.backend.handlers.protontricks_handler import ProtontricksHandler
|
from ..handlers.path_handler import PathHandler
|
||||||
protontricks_handler = ProtontricksHandler(steamdeck or False)
|
|
||||||
result = protontricks_handler.run_protontricks('-l')
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
path_handler = PathHandler()
|
||||||
lines = result.stdout.strip().split('\n')
|
shortcuts_path = path_handler._find_shortcuts_vdf()
|
||||||
|
|
||||||
# Look for our shortcut name in the protontricks output
|
if shortcuts_path:
|
||||||
for line in lines:
|
from ..handlers.vdf_handler import VDFHandler
|
||||||
if shortcut_name in line and 'Non-Steam shortcut:' in line:
|
shortcuts_data = VDFHandler.load(shortcuts_path, binary=True)
|
||||||
# Extract AppID from line like "Non-Steam shortcut: Tuxborn (3106560878)"
|
|
||||||
if '(' in line and ')' in line:
|
|
||||||
appid_str = line.split('(')[1].split(')')[0]
|
|
||||||
actual_appid = int(appid_str)
|
|
||||||
logger.info(f" Found shortcut in protontricks: {line.strip()}")
|
|
||||||
logger.info(f" Initial AppID: {initial_appid}")
|
|
||||||
logger.info(f" Actual AppID: {actual_appid}")
|
|
||||||
return actual_appid
|
|
||||||
|
|
||||||
logger.debug(f"Shortcut '{shortcut_name}' not found in protontricks yet (attempt {i+1}/30)")
|
if shortcuts_data and 'shortcuts' in shortcuts_data:
|
||||||
|
for idx, shortcut in shortcuts_data['shortcuts'].items():
|
||||||
|
app_name = shortcut.get('AppName', shortcut.get('appname', '')).strip()
|
||||||
|
|
||||||
|
if app_name.lower() == shortcut_name.lower():
|
||||||
|
appid = shortcut.get('appid')
|
||||||
|
if appid:
|
||||||
|
actual_appid = int(appid) & 0xFFFFFFFF
|
||||||
|
logger.info(f"Found shortcut '{app_name}' in shortcuts.vdf")
|
||||||
|
logger.info(f" Initial AppID (signed): {initial_appid}")
|
||||||
|
logger.info(f" Actual AppID (unsigned): {actual_appid}")
|
||||||
|
return actual_appid
|
||||||
|
|
||||||
|
logger.debug(f"Shortcut '{shortcut_name}' not found in VDF yet (attempt {i+1}/30)")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
logger.warning(f"protontricks -l timed out on attempt {i+1}")
|
|
||||||
time.sleep(1)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error running protontricks -l on attempt {i+1}: {e}")
|
logger.warning(f"Error reading shortcuts.vdf on attempt {i+1}: {e}")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
logger.error(f"Shortcut '{shortcut_name}' not found in protontricks after 30 seconds")
|
logger.error(f"Shortcut '{shortcut_name}' not found in shortcuts.vdf after 30 seconds")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -15,29 +15,32 @@ _KEYWORD_PHRASES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_non_premium_indicator(line: str) -> bool:
|
def is_non_premium_indicator(line: str) -> tuple[bool, str | None]:
|
||||||
"""
|
"""
|
||||||
Return True if the engine output line indicates a Nexus non-premium scenario.
|
Return True if the engine output line indicates a Nexus non-premium scenario.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
line: Raw line emitted from the jackify-engine process.
|
line: Raw line emitted from the jackify-engine process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_premium_error: bool, matched_pattern: str | None)
|
||||||
"""
|
"""
|
||||||
if not line:
|
if not line:
|
||||||
return False
|
return False, None
|
||||||
|
|
||||||
normalized = line.strip().lower()
|
normalized = line.strip().lower()
|
||||||
if not normalized:
|
if not normalized:
|
||||||
return False
|
return False, None
|
||||||
|
|
||||||
# Direct phrase detection
|
# Direct phrase detection
|
||||||
for phrase in _KEYWORD_PHRASES[:6]:
|
for phrase in _KEYWORD_PHRASES[:6]:
|
||||||
if phrase in normalized:
|
if phrase in normalized:
|
||||||
return True
|
return True, phrase
|
||||||
|
|
||||||
# Manual download + Nexus URL implies premium requirement in current workflows.
|
# Manual download + Nexus URL implies premium requirement in current workflows.
|
||||||
if "manual download" in normalized and ("nexusmods.com" in normalized or "nexus mods" in normalized):
|
if "manual download" in normalized and ("nexusmods.com" in normalized or "nexus mods" in normalized):
|
||||||
return True
|
return True, "manual download + nexusmods.com"
|
||||||
|
|
||||||
return False
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from PySide6.QtWidgets import (
|
|||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtGui import QPixmap, QIcon
|
from PySide6.QtGui import QPixmap, QIcon
|
||||||
|
|
||||||
|
from jackify.shared.paths import get_jackify_logs_dir
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -198,6 +200,6 @@ Modlist Install and Configuration complete!:
|
|||||||
NOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of
|
NOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of
|
||||||
Valve's Proton 10 (known ENB compatibility issues in Valve's Proton 10).
|
Valve's Proton 10 (known ENB compatibility issues in Valve's Proton 10).
|
||||||
|
|
||||||
Detailed log available at: ~/Jackify/logs/Configure_New_Modlist_workflow.log"""
|
Detailed log available at: {get_jackify_logs_dir()}/Configure_New_Modlist_workflow.log"""
|
||||||
|
|
||||||
return completion_text
|
return completion_text
|
||||||
@@ -22,6 +22,7 @@ from jackify.backend.services.api_key_service import APIKeyService
|
|||||||
from jackify.backend.services.resolution_service import ResolutionService
|
from jackify.backend.services.resolution_service import ResolutionService
|
||||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||||
from ..dialogs import SuccessDialog
|
from ..dialogs import SuccessDialog
|
||||||
|
from jackify.frontends.gui.services.message_service import MessageService
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""Print debug message only if debug mode is enabled"""
|
"""Print debug message only if debug mode is enabled"""
|
||||||
@@ -522,22 +523,20 @@ class ConfigureExistingModlistScreen(QWidget):
|
|||||||
message_lower = text.lower()
|
message_lower = text.lower()
|
||||||
|
|
||||||
# Update progress indicator based on key status messages
|
# Update progress indicator based on key status messages
|
||||||
if "creating steam shortcut" in message_lower:
|
if "setting protontricks permissions" in message_lower or "permissions" in message_lower:
|
||||||
self.progress_indicator.set_status("Creating Steam shortcut...", 10)
|
self.progress_indicator.set_status("Setting permissions...", 20)
|
||||||
elif "restarting steam" in message_lower or "restart steam" in message_lower:
|
elif "applying curated registry" in message_lower or "registry" in message_lower:
|
||||||
self.progress_indicator.set_status("Restarting Steam...", 20)
|
self.progress_indicator.set_status("Applying registry files...", 40)
|
||||||
elif "steam restart" in message_lower and "success" in message_lower:
|
elif "installing wine components" in message_lower or "wine component" in message_lower:
|
||||||
self.progress_indicator.set_status("Steam restarted successfully", 30)
|
self.progress_indicator.set_status("Installing wine components...", 60)
|
||||||
elif "creating proton prefix" in message_lower or "prefix creation" in message_lower:
|
elif "dotnet" in message_lower and "fix" in message_lower:
|
||||||
self.progress_indicator.set_status("Creating Proton prefix...", 50)
|
self.progress_indicator.set_status("Applying dotnet fixes...", 75)
|
||||||
elif "prefix created" in message_lower or "prefix creation" in message_lower and "success" in message_lower:
|
elif "setting ownership" in message_lower or "ownership and permissions" in message_lower:
|
||||||
self.progress_indicator.set_status("Proton prefix created", 70)
|
self.progress_indicator.set_status("Setting permissions...", 85)
|
||||||
elif "verifying" in message_lower:
|
elif "verifying" in message_lower:
|
||||||
self.progress_indicator.set_status("Verifying setup...", 80)
|
self.progress_indicator.set_status("Verifying setup...", 90)
|
||||||
elif "steam integration complete" in message_lower or "configuration complete" in message_lower:
|
elif "steam integration complete" in message_lower or "configuration complete" in message_lower:
|
||||||
self.progress_indicator.set_status("Configuration complete", 95)
|
self.progress_indicator.set_status("Configuration complete", 100)
|
||||||
elif "complete" in message_lower and not "prefix" in message_lower:
|
|
||||||
self.progress_indicator.set_status("Finishing up...", 90)
|
|
||||||
|
|
||||||
# Update activity window with generic configuration status
|
# Update activity window with generic configuration status
|
||||||
# Only update if message contains meaningful progress (not blank lines or separators)
|
# Only update if message contains meaningful progress (not blank lines or separators)
|
||||||
|
|||||||
@@ -612,12 +612,18 @@ class ConfigureNewModlistScreen(QWidget):
|
|||||||
self.progress_indicator.set_status("Creating Proton prefix...", 50)
|
self.progress_indicator.set_status("Creating Proton prefix...", 50)
|
||||||
elif "prefix created" in message_lower or "prefix creation" in message_lower and "success" in message_lower:
|
elif "prefix created" in message_lower or "prefix creation" in message_lower and "success" in message_lower:
|
||||||
self.progress_indicator.set_status("Proton prefix created", 70)
|
self.progress_indicator.set_status("Proton prefix created", 70)
|
||||||
|
elif "applying curated registry" in message_lower or "registry" in message_lower:
|
||||||
|
self.progress_indicator.set_status("Applying registry files...", 75)
|
||||||
|
elif "installing wine components" in message_lower or "wine component" in message_lower:
|
||||||
|
self.progress_indicator.set_status("Installing wine components...", 80)
|
||||||
|
elif "dotnet" in message_lower and "fix" in message_lower:
|
||||||
|
self.progress_indicator.set_status("Applying dotnet fixes...", 85)
|
||||||
|
elif "setting ownership" in message_lower or "ownership and permissions" in message_lower:
|
||||||
|
self.progress_indicator.set_status("Setting permissions...", 90)
|
||||||
elif "verifying" in message_lower:
|
elif "verifying" in message_lower:
|
||||||
self.progress_indicator.set_status("Verifying setup...", 80)
|
self.progress_indicator.set_status("Verifying setup...", 95)
|
||||||
elif "steam integration complete" in message_lower or "configuration complete" in message_lower:
|
elif "steam integration complete" in message_lower or "configuration complete" in message_lower:
|
||||||
self.progress_indicator.set_status("Configuration complete", 95)
|
self.progress_indicator.set_status("Configuration complete", 100)
|
||||||
elif "complete" in message_lower and not "prefix" in message_lower:
|
|
||||||
self.progress_indicator.set_status("Finishing up...", 90)
|
|
||||||
|
|
||||||
# Update activity window with generic configuration status
|
# Update activity window with generic configuration status
|
||||||
# Only update if message contains meaningful progress (not blank lines or separators)
|
# Only update if message contains meaningful progress (not blank lines or separators)
|
||||||
|
|||||||
@@ -1881,6 +1881,26 @@ class InstallModlistScreen(QWidget):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Log authentication status at install start (Issue #111 diagnostics)
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
auth_method = self.auth_service.get_auth_method()
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("Authentication Status at Install Start")
|
||||||
|
logger.info(f"Method: {auth_method or 'UNKNOWN'}")
|
||||||
|
logger.info(f"Token length: {len(api_key)} chars")
|
||||||
|
if len(api_key) >= 8:
|
||||||
|
logger.info(f"Token (partial): {api_key[:4]}...{api_key[-4:]}")
|
||||||
|
|
||||||
|
if auth_method == 'oauth':
|
||||||
|
token_handler = self.auth_service.token_handler
|
||||||
|
token_info = token_handler.get_token_info()
|
||||||
|
if 'expires_in_minutes' in token_info:
|
||||||
|
logger.info(f"OAuth expires in: {token_info['expires_in_minutes']:.1f} minutes")
|
||||||
|
if token_info.get('refresh_token_likely_expired'):
|
||||||
|
logger.warning(f"OAuth refresh token age: {token_info['refresh_token_age_days']:.1f} days (may need re-auth)")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
modlist_name = self.modlist_name_edit.text().strip()
|
modlist_name = self.modlist_name_edit.text().strip()
|
||||||
missing_fields = []
|
missing_fields = []
|
||||||
if not modlist_name:
|
if not modlist_name:
|
||||||
@@ -2129,6 +2149,9 @@ class InstallModlistScreen(QWidget):
|
|||||||
# R&D: Progress state manager for parsing
|
# R&D: Progress state manager for parsing
|
||||||
self.progress_state_manager = progress_state_manager
|
self.progress_state_manager = progress_state_manager
|
||||||
self._premium_signal_sent = False
|
self._premium_signal_sent = False
|
||||||
|
# Rolling buffer for Premium detection diagnostics
|
||||||
|
self._engine_output_buffer = []
|
||||||
|
self._buffer_size = 10
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
self.cancelled = True
|
self.cancelled = True
|
||||||
@@ -2196,10 +2219,71 @@ class InstallModlistScreen(QWidget):
|
|||||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||||
config_handler = ConfigHandler()
|
config_handler = ConfigHandler()
|
||||||
debug_mode = config_handler.get('debug_mode', False)
|
debug_mode = config_handler.get('debug_mode', False)
|
||||||
if not self._premium_signal_sent and is_non_premium_indicator(decoded):
|
|
||||||
|
# Check for Premium detection
|
||||||
|
is_premium_error, matched_pattern = is_non_premium_indicator(decoded)
|
||||||
|
if not self._premium_signal_sent and is_premium_error:
|
||||||
self._premium_signal_sent = True
|
self._premium_signal_sent = True
|
||||||
|
|
||||||
|
# DIAGNOSTIC LOGGING: Capture false positive details
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.warning("=" * 80)
|
||||||
|
logger.warning("PREMIUM DETECTION TRIGGERED - DIAGNOSTIC DUMP (Issue #111)")
|
||||||
|
logger.warning("=" * 80)
|
||||||
|
logger.warning(f"Matched pattern: '{matched_pattern}'")
|
||||||
|
logger.warning(f"Triggering line: '{decoded.strip()}'")
|
||||||
|
|
||||||
|
# Detailed auth diagnostics
|
||||||
|
logger.warning("")
|
||||||
|
logger.warning("AUTHENTICATION DIAGNOSTICS:")
|
||||||
|
logger.warning(f" Auth value present: {'YES' if self.api_key else 'NO'}")
|
||||||
|
if self.api_key:
|
||||||
|
logger.warning(f" Auth value length: {len(self.api_key)} chars")
|
||||||
|
if len(self.api_key) >= 8:
|
||||||
|
logger.warning(f" Auth value (partial): {self.api_key[:4]}...{self.api_key[-4:]}")
|
||||||
|
|
||||||
|
# Determine auth method and get detailed status
|
||||||
|
auth_method = self.auth_service.get_auth_method()
|
||||||
|
logger.warning(f" Auth method: {auth_method or 'UNKNOWN'}")
|
||||||
|
|
||||||
|
if auth_method == 'oauth':
|
||||||
|
# Get detailed OAuth token status
|
||||||
|
token_handler = self.auth_service.token_handler
|
||||||
|
token_info = token_handler.get_token_info()
|
||||||
|
|
||||||
|
logger.warning(" OAuth Token Status:")
|
||||||
|
logger.warning(f" Has token file: {token_info.get('has_token', False)}")
|
||||||
|
logger.warning(f" Has refresh token: {token_info.get('has_refresh_token', False)}")
|
||||||
|
|
||||||
|
if 'expires_in_minutes' in token_info:
|
||||||
|
logger.warning(f" Expires in: {token_info['expires_in_minutes']:.1f} minutes")
|
||||||
|
logger.warning(f" Is expired: {token_info.get('is_expired', False)}")
|
||||||
|
logger.warning(f" Expires soon (5min): {token_info.get('expires_soon_5min', False)}")
|
||||||
|
|
||||||
|
if 'refresh_token_age_days' in token_info:
|
||||||
|
logger.warning(f" Refresh token age: {token_info['refresh_token_age_days']:.1f} days")
|
||||||
|
logger.warning(f" Refresh token likely expired: {token_info.get('refresh_token_likely_expired', False)}")
|
||||||
|
|
||||||
|
if token_info.get('error'):
|
||||||
|
logger.warning(f" Error: {token_info['error']}")
|
||||||
|
|
||||||
|
logger.warning("")
|
||||||
|
logger.warning("Previous engine output (last 10 lines):")
|
||||||
|
for i, buffered_line in enumerate(self._engine_output_buffer, 1):
|
||||||
|
logger.warning(f" -{len(self._engine_output_buffer) - i + 1}: {buffered_line}")
|
||||||
|
logger.warning("")
|
||||||
|
logger.warning("If user HAS Premium, this is a FALSE POSITIVE")
|
||||||
|
logger.warning("Report to: https://github.com/Omni-guides/Jackify/issues/111")
|
||||||
|
logger.warning("=" * 80)
|
||||||
|
|
||||||
self.premium_required_detected.emit(decoded.strip() or "Nexus Premium required")
|
self.premium_required_detected.emit(decoded.strip() or "Nexus Premium required")
|
||||||
|
|
||||||
|
# Maintain rolling buffer of engine output for diagnostics
|
||||||
|
self._engine_output_buffer.append(decoded.strip())
|
||||||
|
if len(self._engine_output_buffer) > self._buffer_size:
|
||||||
|
self._engine_output_buffer.pop(0)
|
||||||
|
|
||||||
# R&D: Process through progress parser
|
# R&D: Process through progress parser
|
||||||
if self.progress_state_manager:
|
if self.progress_state_manager:
|
||||||
updated = self.progress_state_manager.process_line(decoded)
|
updated = self.progress_state_manager.process_line(decoded)
|
||||||
@@ -2223,10 +2307,69 @@ class InstallModlistScreen(QWidget):
|
|||||||
decoded = line.decode('utf-8', errors='replace')
|
decoded = line.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
# Notify when Nexus requires Premium before continuing
|
# Notify when Nexus requires Premium before continuing
|
||||||
if not self._premium_signal_sent and is_non_premium_indicator(decoded):
|
is_premium_error, matched_pattern = is_non_premium_indicator(decoded)
|
||||||
|
if not self._premium_signal_sent and is_premium_error:
|
||||||
self._premium_signal_sent = True
|
self._premium_signal_sent = True
|
||||||
|
|
||||||
|
# DIAGNOSTIC LOGGING: Capture false positive details
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.warning("=" * 80)
|
||||||
|
logger.warning("PREMIUM DETECTION TRIGGERED - DIAGNOSTIC DUMP (Issue #111)")
|
||||||
|
logger.warning("=" * 80)
|
||||||
|
logger.warning(f"Matched pattern: '{matched_pattern}'")
|
||||||
|
logger.warning(f"Triggering line: '{decoded.strip()}'")
|
||||||
|
|
||||||
|
# Detailed auth diagnostics
|
||||||
|
logger.warning("")
|
||||||
|
logger.warning("AUTHENTICATION DIAGNOSTICS:")
|
||||||
|
logger.warning(f" Auth value present: {'YES' if self.api_key else 'NO'}")
|
||||||
|
if self.api_key:
|
||||||
|
logger.warning(f" Auth value length: {len(self.api_key)} chars")
|
||||||
|
if len(self.api_key) >= 8:
|
||||||
|
logger.warning(f" Auth value (partial): {self.api_key[:4]}...{self.api_key[-4:]}")
|
||||||
|
|
||||||
|
# Determine auth method and get detailed status
|
||||||
|
auth_method = self.auth_service.get_auth_method()
|
||||||
|
logger.warning(f" Auth method: {auth_method or 'UNKNOWN'}")
|
||||||
|
|
||||||
|
if auth_method == 'oauth':
|
||||||
|
# Get detailed OAuth token status
|
||||||
|
token_handler = self.auth_service.token_handler
|
||||||
|
token_info = token_handler.get_token_info()
|
||||||
|
|
||||||
|
logger.warning(" OAuth Token Status:")
|
||||||
|
logger.warning(f" Has token file: {token_info.get('has_token', False)}")
|
||||||
|
logger.warning(f" Has refresh token: {token_info.get('has_refresh_token', False)}")
|
||||||
|
|
||||||
|
if 'expires_in_minutes' in token_info:
|
||||||
|
logger.warning(f" Expires in: {token_info['expires_in_minutes']:.1f} minutes")
|
||||||
|
logger.warning(f" Is expired: {token_info.get('is_expired', False)}")
|
||||||
|
logger.warning(f" Expires soon (5min): {token_info.get('expires_soon_5min', False)}")
|
||||||
|
|
||||||
|
if 'refresh_token_age_days' in token_info:
|
||||||
|
logger.warning(f" Refresh token age: {token_info['refresh_token_age_days']:.1f} days")
|
||||||
|
logger.warning(f" Refresh token likely expired: {token_info.get('refresh_token_likely_expired', False)}")
|
||||||
|
|
||||||
|
if token_info.get('error'):
|
||||||
|
logger.warning(f" Error: {token_info['error']}")
|
||||||
|
|
||||||
|
logger.warning("")
|
||||||
|
logger.warning("Previous engine output (last 10 lines):")
|
||||||
|
for i, buffered_line in enumerate(self._engine_output_buffer, 1):
|
||||||
|
logger.warning(f" -{len(self._engine_output_buffer) - i + 1}: {buffered_line}")
|
||||||
|
logger.warning("")
|
||||||
|
logger.warning("If user HAS Premium, this is a FALSE POSITIVE")
|
||||||
|
logger.warning("Report to: https://github.com/Omni-guides/Jackify/issues/111")
|
||||||
|
logger.warning("=" * 80)
|
||||||
|
|
||||||
self.premium_required_detected.emit(decoded.strip() or "Nexus Premium required")
|
self.premium_required_detected.emit(decoded.strip() or "Nexus Premium required")
|
||||||
|
|
||||||
|
# Maintain rolling buffer of engine output for diagnostics
|
||||||
|
self._engine_output_buffer.append(decoded.strip())
|
||||||
|
if len(self._engine_output_buffer) > self._buffer_size:
|
||||||
|
self._engine_output_buffer.pop(0)
|
||||||
|
|
||||||
# R&D: Process through progress parser
|
# R&D: Process through progress parser
|
||||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||||
config_handler = ConfigHandler()
|
config_handler = ConfigHandler()
|
||||||
|
|||||||
@@ -492,27 +492,13 @@ class FileProgressList(QWidget):
|
|||||||
|
|
||||||
# Widget doesn't exist - create it (only clear when creating new widget)
|
# Widget doesn't exist - create it (only clear when creating new widget)
|
||||||
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
||||||
_debug_log(f"[WIDGET_FIX] About to clear list_widget for summary widget - count={self.list_widget.count()}")
|
|
||||||
for i in range(self.list_widget.count()):
|
for i in range(self.list_widget.count()):
|
||||||
item = self.list_widget.item(i)
|
item = self.list_widget.item(i)
|
||||||
if item:
|
if item:
|
||||||
widget = self.list_widget.itemWidget(item)
|
widget = self.list_widget.itemWidget(item)
|
||||||
if widget:
|
if widget:
|
||||||
_debug_log(f"[WIDGET_FIX] Removing widget before clear (summary) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
self.list_widget.removeItemWidget(item)
|
self.list_widget.removeItemWidget(item)
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self.list_widget.clear()
|
self.list_widget.clear()
|
||||||
# Check widgets in _file_items dict after clear
|
|
||||||
for key, widget in list(self._file_items.items()):
|
|
||||||
if widget:
|
|
||||||
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self._file_items.clear()
|
self._file_items.clear()
|
||||||
|
|
||||||
# Create new summary widget
|
# Create new summary widget
|
||||||
@@ -541,19 +527,8 @@ class FileProgressList(QWidget):
|
|||||||
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
||||||
widget = self.list_widget.itemWidget(item)
|
widget = self.list_widget.itemWidget(item)
|
||||||
if widget:
|
if widget:
|
||||||
_debug_log(f"[WIDGET_FIX] Removing summary widget - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
self.list_widget.removeItemWidget(item)
|
self.list_widget.removeItemWidget(item)
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Summary widget became top-level window after removeItemWidget()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self.list_widget.takeItem(i)
|
self.list_widget.takeItem(i)
|
||||||
if widget:
|
|
||||||
_debug_log(f"[WIDGET_FIX] After takeItem (summary) - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Summary widget is still a top-level window after takeItem()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
break
|
break
|
||||||
self._summary_widget = None
|
self._summary_widget = None
|
||||||
else:
|
else:
|
||||||
@@ -568,19 +543,8 @@ class FileProgressList(QWidget):
|
|||||||
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
||||||
widget = self.list_widget.itemWidget(item)
|
widget = self.list_widget.itemWidget(item)
|
||||||
if widget:
|
if widget:
|
||||||
_debug_log(f"[WIDGET_FIX] Removing transition label - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
self.list_widget.removeItemWidget(item)
|
self.list_widget.removeItemWidget(item)
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Transition label became top-level window after removeItemWidget()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self.list_widget.takeItem(i)
|
self.list_widget.takeItem(i)
|
||||||
if widget:
|
|
||||||
_debug_log(f"[WIDGET_FIX] After takeItem (transition) - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Transition label is still a top-level window after takeItem()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
break
|
break
|
||||||
self._transition_label = None
|
self._transition_label = None
|
||||||
|
|
||||||
@@ -592,27 +556,13 @@ class FileProgressList(QWidget):
|
|||||||
else:
|
else:
|
||||||
# Show empty state but keep header stable
|
# Show empty state but keep header stable
|
||||||
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
||||||
_debug_log(f"[WIDGET_FIX] About to clear list_widget (empty state) - count={self.list_widget.count()}")
|
|
||||||
for i in range(self.list_widget.count()):
|
for i in range(self.list_widget.count()):
|
||||||
item = self.list_widget.item(i)
|
item = self.list_widget.item(i)
|
||||||
if item:
|
if item:
|
||||||
widget = self.list_widget.itemWidget(item)
|
widget = self.list_widget.itemWidget(item)
|
||||||
if widget:
|
if widget:
|
||||||
_debug_log(f"[WIDGET_FIX] Removing widget before clear (empty) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
self.list_widget.removeItemWidget(item)
|
self.list_widget.removeItemWidget(item)
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self.list_widget.clear()
|
self.list_widget.clear()
|
||||||
# Check widgets in _file_items dict after clear
|
|
||||||
for key, widget in list(self._file_items.items()):
|
|
||||||
if widget:
|
|
||||||
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear (empty) - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self._file_items.clear()
|
self._file_items.clear()
|
||||||
|
|
||||||
# Update last phase tracker
|
# Update last phase tracker
|
||||||
@@ -661,21 +611,8 @@ class FileProgressList(QWidget):
|
|||||||
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
||||||
widget = self.list_widget.itemWidget(item)
|
widget = self.list_widget.itemWidget(item)
|
||||||
if widget:
|
if widget:
|
||||||
_debug_log(f"[WIDGET_FIX] Removing widget for item_key={item_key} - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
self.list_widget.removeItemWidget(item)
|
self.list_widget.removeItemWidget(item)
|
||||||
# Check if widget became orphaned after removal
|
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget()! widget={widget}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self.list_widget.takeItem(i)
|
self.list_widget.takeItem(i)
|
||||||
# Final check after takeItem
|
|
||||||
if widget:
|
|
||||||
_debug_log(f"[WIDGET_FIX] After takeItem - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget is still a top-level window after takeItem()! This is the bug!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
break
|
break
|
||||||
del self._file_items[item_key]
|
del self._file_items[item_key]
|
||||||
|
|
||||||
@@ -735,27 +672,13 @@ class FileProgressList(QWidget):
|
|||||||
def _show_transition_message(self, new_phase: str):
|
def _show_transition_message(self, new_phase: str):
|
||||||
"""Show a brief 'Preparing...' message during phase transitions."""
|
"""Show a brief 'Preparing...' message during phase transitions."""
|
||||||
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
||||||
_debug_log(f"[WIDGET_FIX] About to clear list_widget (transition) - count={self.list_widget.count()}")
|
|
||||||
for i in range(self.list_widget.count()):
|
for i in range(self.list_widget.count()):
|
||||||
item = self.list_widget.item(i)
|
item = self.list_widget.item(i)
|
||||||
if item:
|
if item:
|
||||||
widget = self.list_widget.itemWidget(item)
|
widget = self.list_widget.itemWidget(item)
|
||||||
if widget:
|
if widget:
|
||||||
_debug_log(f"[WIDGET_FIX] Removing widget before clear (transition) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
self.list_widget.removeItemWidget(item)
|
self.list_widget.removeItemWidget(item)
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self.list_widget.clear()
|
self.list_widget.clear()
|
||||||
# Check widgets in _file_items dict after clear
|
|
||||||
for key, widget in list(self._file_items.items()):
|
|
||||||
if widget:
|
|
||||||
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear (transition) - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self._file_items.clear()
|
self._file_items.clear()
|
||||||
|
|
||||||
# Header removed - tab label provides context
|
# Header removed - tab label provides context
|
||||||
@@ -781,41 +704,15 @@ class FileProgressList(QWidget):
|
|||||||
def clear(self):
|
def clear(self):
|
||||||
"""Clear all file items."""
|
"""Clear all file items."""
|
||||||
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
||||||
_debug_log(f"[WIDGET_FIX] clear() called - count={self.list_widget.count()}")
|
|
||||||
for i in range(self.list_widget.count()):
|
for i in range(self.list_widget.count()):
|
||||||
item = self.list_widget.item(i)
|
item = self.list_widget.item(i)
|
||||||
if item:
|
if item:
|
||||||
widget = self.list_widget.itemWidget(item)
|
widget = self.list_widget.itemWidget(item)
|
||||||
if widget:
|
if widget:
|
||||||
_debug_log(f"[WIDGET_FIX] Removing widget before clear() - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
self.list_widget.removeItemWidget(item)
|
self.list_widget.removeItemWidget(item)
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self.list_widget.clear()
|
self.list_widget.clear()
|
||||||
# Check widgets in _file_items dict after clear
|
|
||||||
for key, widget in list(self._file_items.items()):
|
|
||||||
if widget:
|
|
||||||
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear() - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
|
||||||
if widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self._file_items.clear()
|
self._file_items.clear()
|
||||||
if self._summary_widget:
|
|
||||||
_debug_log(f"[WIDGET_FIX] Clearing summary_widget - widget={self._summary_widget}, parent={self._summary_widget.parent()}, isWindow()={self._summary_widget.isWindow()}")
|
|
||||||
if self._summary_widget.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Summary widget is a top-level window in clear()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self._summary_widget = None
|
self._summary_widget = None
|
||||||
if self._transition_label:
|
|
||||||
_debug_log(f"[WIDGET_FIX] Clearing transition_label - widget={self._transition_label}, parent={self._transition_label.parent()}, isWindow()={self._transition_label.isWindow()}")
|
|
||||||
if self._transition_label.isWindow():
|
|
||||||
print(f"[WIDGET_FIX] ERROR: Transition label is a top-level window in clear()!")
|
|
||||||
import traceback
|
|
||||||
traceback.print_stack()
|
|
||||||
self._transition_label = None
|
self._transition_label = None
|
||||||
self._last_phase = None
|
self._last_phase = None
|
||||||
# Header removed - tab label provides context
|
# Header removed - tab label provides context
|
||||||
|
|||||||
Reference in New Issue
Block a user