mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:07:45 +02:00
682 lines
31 KiB
Python
682 lines
31 KiB
Python
"""TTW installation workflow methods for InstallTTWScreen (Mixin)."""
|
|
from pathlib import Path
|
|
from PySide6.QtCore import QTimer, Qt, QThread, Signal, QProcess
|
|
from PySide6.QtWidgets import QMessageBox, QApplication
|
|
from PySide6.QtGui import QTextCursor
|
|
import logging
|
|
import os
|
|
import re
|
|
import time
|
|
import traceback
|
|
import shutil
|
|
import tempfile
|
|
# Runtime imports to avoid circular dependencies
|
|
from jackify.frontends.gui.services.message_service import MessageService # Runtime import
|
|
from jackify.backend.handlers.validation_handler import ValidationHandler # Runtime import
|
|
from jackify.frontends.gui.dialogs.warning_dialog import WarningDialog # Runtime import
|
|
from ..shared_theme import JACKIFY_COLOR_BLUE # Runtime import
|
|
from ..utils import strip_ansi_control_codes # Runtime import
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def debug_print(message):
|
|
"""Print debug message only if debug mode is enabled"""
|
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
|
config_handler = ConfigHandler()
|
|
if config_handler.get('debug_mode', False):
|
|
print(message)
|
|
|
|
|
|
class TTWWorkflowMixin:
|
|
"""Mixin providing installation workflow methods for InstallTTWScreen."""
|
|
|
|
def validate_and_start_install(self):
|
|
import time
|
|
self._install_workflow_start_time = time.time()
|
|
debug_print('DEBUG: validate_and_start_install called')
|
|
|
|
# Reload config to pick up any settings changes made in Settings dialog
|
|
self.config_handler.reload_config()
|
|
debug_print('DEBUG: Reloaded config from disk')
|
|
|
|
# Check TTW requirements first
|
|
if not self._check_ttw_requirements():
|
|
return
|
|
|
|
# Check protontricks before proceeding
|
|
if not self._check_protontricks():
|
|
return
|
|
|
|
# Disable all controls during installation (except Cancel)
|
|
self._disable_controls_during_operation()
|
|
|
|
try:
|
|
# TTW only needs .mpi file
|
|
mpi_path = self.file_edit.text().strip()
|
|
if not mpi_path or not os.path.isfile(mpi_path) or not mpi_path.endswith('.mpi'):
|
|
MessageService.warning(self, "Invalid TTW File", "Please select a valid TTW .mpi file.")
|
|
self._enable_controls_after_operation()
|
|
return
|
|
install_dir = self.install_dir_edit.text().strip()
|
|
|
|
# Validate required fields
|
|
missing_fields = []
|
|
if not install_dir:
|
|
missing_fields.append("Install Directory")
|
|
if missing_fields:
|
|
MessageService.warning(self, "Missing Required Fields", f"Please fill in all required fields before starting the install:\n- " + "\n- ".join(missing_fields))
|
|
self._enable_controls_after_operation()
|
|
return
|
|
|
|
# Validate install directory
|
|
validation_handler = ValidationHandler()
|
|
from pathlib import Path
|
|
install_dir_path = Path(install_dir)
|
|
|
|
# Check for dangerous directories first (system roots, etc.)
|
|
if validation_handler.is_dangerous_directory(install_dir_path):
|
|
dlg = WarningDialog(
|
|
f"The directory '{install_dir}' is a system or user root and cannot be used for TTW installation.",
|
|
parent=self
|
|
)
|
|
if not dlg.exec() or not dlg.confirmed:
|
|
self._enable_controls_after_operation()
|
|
return
|
|
|
|
# Check if directory exists and is not empty - TTW_Linux_Installer will overwrite existing files
|
|
if install_dir_path.exists() and install_dir_path.is_dir():
|
|
# Check if directory contains any files
|
|
try:
|
|
has_files = any(install_dir_path.iterdir())
|
|
if has_files:
|
|
# Directory exists and is not empty - warn user about deletion
|
|
dlg = WarningDialog(
|
|
f"The TTW output directory already exists and contains files:\n{install_dir}\n\n"
|
|
f"All files in this directory will be deleted before installation.\n\n"
|
|
f"This action cannot be undone.",
|
|
parent=self
|
|
)
|
|
if not dlg.exec() or not dlg.confirmed:
|
|
self._enable_controls_after_operation()
|
|
return
|
|
|
|
# User confirmed - delete all contents of the directory
|
|
import shutil
|
|
try:
|
|
for item in install_dir_path.iterdir():
|
|
if item.is_dir():
|
|
shutil.rmtree(item)
|
|
else:
|
|
item.unlink()
|
|
debug_print(f"DEBUG: Deleted all contents of {install_dir}")
|
|
except Exception as e:
|
|
MessageService.critical(self, "Error", f"Failed to delete directory contents:\n{e}")
|
|
self._enable_controls_after_operation()
|
|
return
|
|
except Exception as e:
|
|
debug_print(f"DEBUG: Error checking directory contents: {e}")
|
|
# If we can't check, proceed
|
|
|
|
if not os.path.isdir(install_dir):
|
|
create = MessageService.question(self, "Create Directory?",
|
|
f"The install directory does not exist:\n{install_dir}\n\nWould you like to create it?",
|
|
critical=False # Non-critical, won't steal focus
|
|
)
|
|
if create == QMessageBox.Yes:
|
|
try:
|
|
os.makedirs(install_dir, exist_ok=True)
|
|
except Exception as e:
|
|
MessageService.critical(self, "Error", f"Failed to create install directory:\n{e}")
|
|
self._enable_controls_after_operation()
|
|
return
|
|
else:
|
|
self._enable_controls_after_operation()
|
|
return
|
|
|
|
# Start TTW installation
|
|
self.console.clear()
|
|
self.process_monitor.clear()
|
|
|
|
# Update button states for installation
|
|
self.start_btn.setEnabled(False)
|
|
self.cancel_btn.setVisible(False)
|
|
self.cancel_install_btn.setVisible(True)
|
|
|
|
debug_print(f'DEBUG: Calling run_ttw_installer with mpi_path={mpi_path}, install_dir={install_dir}')
|
|
self.run_ttw_installer(mpi_path, install_dir)
|
|
except Exception as e:
|
|
debug_print(f"DEBUG: Exception in validate_and_start_install: {e}")
|
|
import traceback
|
|
debug_print(f"DEBUG: Traceback: {traceback.format_exc()}")
|
|
# Re-enable all controls after exception
|
|
self._enable_controls_after_operation()
|
|
self.cancel_btn.setVisible(True)
|
|
self.cancel_install_btn.setVisible(False)
|
|
debug_print(f"DEBUG: Controls re-enabled in exception handler")
|
|
|
|
def run_ttw_installer(self, mpi_path, install_dir):
|
|
debug_print('DEBUG: run_ttw_installer called - USING THREADED BACKEND WRAPPER')
|
|
|
|
# CRITICAL: Reload config from disk to pick up any settings changes from Settings dialog
|
|
# Refresh Proton version and winetricks settings
|
|
self.config_handler._load_config()
|
|
|
|
# Rotate log file at start of each workflow run (keep 5 backups)
|
|
from jackify.backend.handlers.logging_handler import LoggingHandler
|
|
from pathlib import Path
|
|
log_handler = LoggingHandler()
|
|
log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5)
|
|
|
|
# Clear console for fresh installation output
|
|
self.console.clear()
|
|
self._safe_append_text("Starting TTW installation...")
|
|
|
|
# Initialize Activity window with immediate feedback
|
|
self.file_progress_list.clear()
|
|
self._update_ttw_phase("Initializing TTW installation", 0, 0, 0)
|
|
# Force UI update immediately
|
|
QApplication.processEvents()
|
|
|
|
# Show status banner and show details checkbox
|
|
self.status_banner.setVisible(True)
|
|
self.status_banner.setText("Initializing TTW installation...")
|
|
self.show_details_checkbox.setVisible(True)
|
|
|
|
# Reset banner to default blue color for new installation
|
|
self.status_banner.setStyleSheet(f"""
|
|
background-color: #2a2a2a;
|
|
color: {JACKIFY_COLOR_BLUE};
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
""")
|
|
|
|
self.ttw_start_time = time.time()
|
|
|
|
# Start a timer to update elapsed time
|
|
self.ttw_elapsed_timer = QTimer()
|
|
self.ttw_elapsed_timer.timeout.connect(self._update_ttw_elapsed_time)
|
|
self.ttw_elapsed_timer.start(1000) # Update every second
|
|
|
|
# Update UI state for installation
|
|
self.start_btn.setEnabled(False)
|
|
self.cancel_btn.setVisible(False)
|
|
self.cancel_install_btn.setVisible(True)
|
|
|
|
# Create installation thread
|
|
from PySide6.QtCore import QThread, Signal
|
|
|
|
class TTWInstallationThread(QThread):
|
|
output_batch_received = Signal(list) # Batched output lines
|
|
progress_received = Signal(str)
|
|
installation_finished = Signal(bool, str)
|
|
|
|
def __init__(self, mpi_path, install_dir):
|
|
super().__init__()
|
|
self.mpi_path = mpi_path
|
|
self.install_dir = install_dir
|
|
self.cancelled = False
|
|
self.proc = None
|
|
self.output_buffer = [] # Buffer for batching output
|
|
self.last_emit_time = 0 # Track when we last emitted
|
|
|
|
def cancel(self):
|
|
self.cancelled = True
|
|
try:
|
|
if self.proc and self.proc.poll() is None:
|
|
self.proc.terminate()
|
|
except Exception:
|
|
pass
|
|
|
|
def process_and_buffer_line(self, raw_line):
|
|
"""Process line in worker thread and add to buffer"""
|
|
# Strip ANSI codes
|
|
cleaned = strip_ansi_control_codes(raw_line).strip()
|
|
|
|
# Strip emojis (do this in worker thread, not UI thread)
|
|
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()
|
|
|
|
# Only buffer non-empty lines
|
|
if cleaned:
|
|
self.output_buffer.append(cleaned)
|
|
|
|
def flush_output_buffer(self):
|
|
"""Emit buffered lines as a batch"""
|
|
if self.output_buffer:
|
|
self.output_batch_received.emit(self.output_buffer[:])
|
|
self.output_buffer.clear()
|
|
self.last_emit_time = time.time()
|
|
|
|
def run(self):
|
|
try:
|
|
from jackify.backend.handlers.ttw_installer_handler import TTWInstallerHandler
|
|
from jackify.backend.handlers.filesystem_handler import FileSystemHandler
|
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
|
from pathlib import Path
|
|
import tempfile
|
|
|
|
# Emit startup message
|
|
self.process_and_buffer_line("Initializing TTW installation...")
|
|
self.flush_output_buffer()
|
|
|
|
# Create backend handler
|
|
filesystem_handler = FileSystemHandler()
|
|
config_handler = ConfigHandler()
|
|
ttw_handler = TTWInstallerHandler(
|
|
steamdeck=False,
|
|
verbose=False,
|
|
filesystem_handler=filesystem_handler,
|
|
config_handler=config_handler
|
|
)
|
|
|
|
# Create temporary output file
|
|
output_file = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.ttw_output', encoding='utf-8')
|
|
output_file_path = Path(output_file.name)
|
|
output_file.close()
|
|
|
|
# Start installation via backend (non-blocking)
|
|
self.process_and_buffer_line("Starting TTW installation...")
|
|
self.flush_output_buffer()
|
|
|
|
self.proc, error_msg = ttw_handler.start_ttw_installation(
|
|
Path(self.mpi_path),
|
|
Path(self.install_dir),
|
|
output_file_path
|
|
)
|
|
|
|
if not self.proc:
|
|
self.installation_finished.emit(False, error_msg or "Failed to start TTW installation")
|
|
return
|
|
|
|
self.process_and_buffer_line("TTW_Linux_Installer process started, monitoring output...")
|
|
self.flush_output_buffer()
|
|
|
|
# Poll output file with batching for UI responsiveness
|
|
last_position = 0
|
|
BATCH_INTERVAL = 0.3 # Emit batches every 300ms
|
|
|
|
while self.proc.poll() is None:
|
|
if self.cancelled:
|
|
break
|
|
|
|
try:
|
|
# Read new content from file
|
|
with open(output_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
f.seek(last_position)
|
|
new_lines = f.readlines()
|
|
last_position = f.tell()
|
|
|
|
# Process lines in worker thread (heavy work done here, not UI thread)
|
|
for line in new_lines:
|
|
if self.cancelled:
|
|
break
|
|
self.process_and_buffer_line(line.rstrip())
|
|
|
|
# Emit batch if enough time has passed
|
|
current_time = time.time()
|
|
if current_time - self.last_emit_time >= BATCH_INTERVAL:
|
|
self.flush_output_buffer()
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
# Sleep longer since we're batching
|
|
time.sleep(0.1)
|
|
|
|
# Read any remaining output
|
|
try:
|
|
with open(output_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
f.seek(last_position)
|
|
remaining_lines = f.readlines()
|
|
for line in remaining_lines:
|
|
self.process_and_buffer_line(line.rstrip())
|
|
self.flush_output_buffer()
|
|
except Exception:
|
|
pass
|
|
|
|
# Clean up
|
|
try:
|
|
output_file_path.unlink(missing_ok=True)
|
|
except Exception:
|
|
pass
|
|
|
|
ttw_handler.cleanup_ttw_process(self.proc)
|
|
|
|
# Check result
|
|
returncode = self.proc.returncode if self.proc else -1
|
|
if self.cancelled:
|
|
self.installation_finished.emit(False, "Installation cancelled by user")
|
|
elif returncode == 0:
|
|
self.installation_finished.emit(True, "TTW installation completed successfully!")
|
|
else:
|
|
self.installation_finished.emit(False, f"TTW installation failed with exit code {returncode}")
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
self.installation_finished.emit(False, f"Installation error: {str(e)}")
|
|
|
|
# Start the installation thread
|
|
self.install_thread = TTWInstallationThread(mpi_path, install_dir)
|
|
# Use QueuedConnection to ensure signals are processed asynchronously and don't block UI
|
|
self.install_thread.output_batch_received.connect(self.on_installation_output_batch, Qt.QueuedConnection)
|
|
self.install_thread.progress_received.connect(self.on_installation_progress, Qt.QueuedConnection)
|
|
self.install_thread.installation_finished.connect(self.on_installation_finished, Qt.QueuedConnection)
|
|
|
|
# Start thread and immediately process events to show initial UI state
|
|
self.install_thread.start()
|
|
QApplication.processEvents() # Process any pending events to update UI immediately
|
|
|
|
def on_installation_output_batch(self, messages):
|
|
"""Handle batched output from TTW_Linux_Installer (already processed in worker thread)"""
|
|
# Lines are already cleaned (ANSI codes stripped, emojis removed) in worker thread
|
|
# CRITICAL: Accumulate all console updates and do ONE widget update per batch
|
|
|
|
if not hasattr(self, '_ttw_seen_lines'):
|
|
self._ttw_seen_lines = set()
|
|
self._ttw_current_phase = None
|
|
self._ttw_last_progress = 0
|
|
self._ttw_last_activity_update = 0
|
|
self.ttw_start_time = time.time()
|
|
|
|
# Accumulate lines to display (do ONE console update at end)
|
|
lines_to_display = []
|
|
html_fragments = []
|
|
show_details_due_to_error = False
|
|
latest_progress = None # Track latest progress to update activity ONCE per batch
|
|
|
|
for cleaned in messages:
|
|
if not cleaned:
|
|
continue
|
|
|
|
lower_cleaned = cleaned.lower()
|
|
|
|
# Extract progress (but don't update UI yet - wait until end of batch)
|
|
try:
|
|
progress_match = re.search(r'\[(\d+)/(\d+)\]', cleaned)
|
|
if progress_match:
|
|
current = int(progress_match.group(1))
|
|
total = int(progress_match.group(2))
|
|
percent = int((current / total) * 100) if total > 0 else 0
|
|
latest_progress = (current, total, percent)
|
|
|
|
if 'loading manifest:' in lower_cleaned:
|
|
manifest_match = re.search(r'loading manifest:\s*(\d+)/(\d+)', lower_cleaned)
|
|
if manifest_match:
|
|
current = int(manifest_match.group(1))
|
|
total = int(manifest_match.group(2))
|
|
self._ttw_current_phase = "Loading manifest"
|
|
except Exception:
|
|
pass
|
|
|
|
# Determine if we should show this line
|
|
is_error = 'error:' in lower_cleaned and 'succeeded' not in lower_cleaned and '0 failed' not in lower_cleaned
|
|
is_warning = 'warning:' in lower_cleaned
|
|
is_milestone = any(kw in lower_cleaned for kw in ['===', 'complete', 'finished', 'validation', 'configuration valid'])
|
|
is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx'])
|
|
|
|
# Filter out meaningless standalone messages (just "OK", etc.)
|
|
is_noise = cleaned.strip().upper() in ['OK', 'OK.', 'OK!', 'DONE', 'DONE.', 'SUCCESS', 'SUCCESS.']
|
|
|
|
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op and not is_noise)
|
|
|
|
if should_show:
|
|
if is_error or is_warning:
|
|
color = '#f44336' if is_error else '#ff9800'
|
|
prefix = "WARNING: " if is_warning else "ERROR: "
|
|
escaped = (prefix + cleaned).replace('&', '&').replace('<', '<').replace('>', '>')
|
|
html_fragments.append(f'<span style="color: {color};">{escaped}</span>')
|
|
show_details_due_to_error = True
|
|
else:
|
|
lines_to_display.append(cleaned)
|
|
|
|
# Update activity widget ONCE per batch (if progress changed significantly)
|
|
if latest_progress:
|
|
current, total, percent = latest_progress
|
|
current_time = time.time()
|
|
percent_changed = abs(percent - self._ttw_last_progress) >= 1
|
|
time_passed = (current_time - self._ttw_last_activity_update) >= 0.5 # 500ms throttle
|
|
|
|
if percent_changed or time_passed:
|
|
self._update_ttw_activity(current, total, percent)
|
|
self._ttw_last_progress = percent
|
|
self._ttw_last_activity_update = current_time
|
|
|
|
# Now do ONE console update for entire batch
|
|
if html_fragments or lines_to_display:
|
|
try:
|
|
# Update console with all accumulated output in one operation
|
|
if html_fragments:
|
|
combined_html = '<br>'.join(html_fragments)
|
|
self.console.insertHtml(combined_html + '<br>')
|
|
|
|
if lines_to_display:
|
|
combined_text = '\n'.join(lines_to_display)
|
|
self.console.append(combined_text)
|
|
|
|
if show_details_due_to_error and not self.show_details_checkbox.isChecked():
|
|
self.show_details_checkbox.setChecked(True)
|
|
except Exception:
|
|
pass
|
|
|
|
def on_installation_output(self, message):
|
|
"""Handle regular output from TTW_Linux_Installer with comprehensive filtering and smart parsing"""
|
|
# Initialize tracking structures
|
|
if not hasattr(self, '_ttw_seen_lines'):
|
|
self._ttw_seen_lines = set()
|
|
self._ttw_last_extraction_progress = 0
|
|
self._ttw_last_file_operation_time = 0
|
|
self._ttw_file_operation_count = 0
|
|
self._ttw_current_phase = None
|
|
self._ttw_last_progress_line = None
|
|
self._ttw_progress_line_text = None
|
|
|
|
# Filter out internal status messages from user console
|
|
if message.strip().startswith('[Jackify]'):
|
|
# Log internal messages to file but don't show in console
|
|
self._write_to_log_file(message)
|
|
return
|
|
|
|
# Strip ANSI terminal control codes
|
|
cleaned = strip_ansi_control_codes(message).strip()
|
|
|
|
# Strip emojis from output (TTW_Linux_Installer includes emojis)
|
|
# Use character-by-character filtering to avoid regex recursion issues
|
|
# Safer than regex for emoji removal
|
|
filtered_chars = []
|
|
for char in cleaned:
|
|
code = ord(char)
|
|
# Check if character is in emoji ranges - skip emojis
|
|
is_emoji = (
|
|
(0x1F300 <= code <= 0x1F9FF) or # Miscellaneous Symbols and Pictographs
|
|
(0x1F600 <= code <= 0x1F64F) or # Emoticons
|
|
(0x2600 <= code <= 0x26FF) or # Miscellaneous Symbols
|
|
(0x2700 <= code <= 0x27BF) # Dingbats
|
|
)
|
|
if not is_emoji:
|
|
filtered_chars.append(char)
|
|
cleaned = ''.join(filtered_chars).strip()
|
|
|
|
# Filter out empty lines
|
|
if not cleaned:
|
|
return
|
|
|
|
# Initialize start time if not set
|
|
if not hasattr(self, 'ttw_start_time'):
|
|
self.ttw_start_time = time.time()
|
|
|
|
lower_cleaned = cleaned.lower()
|
|
|
|
# === MINIMAL PROCESSING: Match standalone behavior as closely as possible ===
|
|
# When running standalone: output goes directly to terminal, no processing
|
|
# Here: We must process each line, but do it as efficiently as possible
|
|
|
|
# Always log to file (simple, no recursion risk)
|
|
try:
|
|
self._write_to_log_file(cleaned)
|
|
except Exception:
|
|
pass
|
|
|
|
# Extract progress for Activity window (minimal regex, wrapped in try/except)
|
|
try:
|
|
# Try [X/Y] pattern
|
|
progress_match = re.search(r'\[(\d+)/(\d+)\]', cleaned)
|
|
if progress_match:
|
|
current = int(progress_match.group(1))
|
|
total = int(progress_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"
|
|
if 'loading manifest:' in lower_cleaned:
|
|
manifest_match = re.search(r'loading manifest:\s*(\d+)/(\d+)', lower_cleaned)
|
|
if manifest_match:
|
|
current = int(manifest_match.group(1))
|
|
total = int(manifest_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 Exception:
|
|
pass # Skip if regex fails
|
|
|
|
# Determine if we should show this line
|
|
# By default: only show errors, warnings, milestones
|
|
# Everything else: only in details mode
|
|
is_error = 'error:' in lower_cleaned and 'succeeded' not in lower_cleaned and '0 failed' not in lower_cleaned
|
|
is_warning = 'warning:' in lower_cleaned
|
|
is_milestone = any(kw in lower_cleaned for kw in ['===', 'complete', 'finished', 'validation', 'configuration valid'])
|
|
is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx'])
|
|
|
|
# Filter out meaningless standalone messages (just "OK", etc.)
|
|
is_noise = cleaned.strip().upper() in ['OK', 'OK.', 'OK!', 'DONE', 'DONE.', 'SUCCESS', 'SUCCESS.']
|
|
|
|
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op and not is_noise)
|
|
|
|
if should_show:
|
|
# Direct console append - no recursion, no complex processing
|
|
try:
|
|
if is_error or is_warning:
|
|
# Color code errors/warnings
|
|
color = '#f44336' if is_error else '#ff9800'
|
|
prefix = "WARNING: " if is_warning else "ERROR: "
|
|
escaped = (prefix + cleaned).replace('&', '&').replace('<', '<').replace('>', '>')
|
|
html = f'<span style="color: {color};">{escaped}</span><br>'
|
|
self.console.insertHtml(html)
|
|
if not self.show_details_checkbox.isChecked():
|
|
self.show_details_checkbox.setChecked(True)
|
|
else:
|
|
self.console.append(cleaned)
|
|
except Exception:
|
|
pass # Don't break on console errors
|
|
|
|
return
|
|
|
|
def on_installation_progress(self, progress_message):
|
|
"""Replace the last line in the console for progress updates"""
|
|
cursor = self.console.textCursor()
|
|
cursor.movePosition(QTextCursor.End)
|
|
cursor.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor)
|
|
cursor.removeSelectedText()
|
|
cursor.insertText(progress_message)
|
|
# Don't force scroll for progress updates - let user control
|
|
|
|
def on_installation_finished(self, success, message):
|
|
"""Handle installation completion"""
|
|
debug_print(f"DEBUG: on_installation_finished called with success={success}, message={message}")
|
|
|
|
# Stop elapsed timer
|
|
if hasattr(self, 'ttw_elapsed_timer'):
|
|
self.ttw_elapsed_timer.stop()
|
|
|
|
# Update status banner
|
|
if success:
|
|
elapsed = int(time.time() - self.ttw_start_time) if hasattr(self, 'ttw_start_time') else 0
|
|
minutes = elapsed // 60
|
|
seconds = elapsed % 60
|
|
self.status_banner.setText(f"Installation completed successfully! Total time: {minutes}m {seconds}s")
|
|
self.status_banner.setStyleSheet(f"""
|
|
background-color: #1a4d1a;
|
|
color: #4CAF50;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
""")
|
|
self._safe_append_text(f"\nSuccess: {message}")
|
|
self.process_finished(0, QProcess.NormalExit)
|
|
else:
|
|
self.status_banner.setText(f"Installation failed: {message}")
|
|
self.status_banner.setStyleSheet(f"""
|
|
background-color: #4d1a1a;
|
|
color: #f44336;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
""")
|
|
self._safe_append_text(f"\nError: {message}")
|
|
self.process_finished(1, QProcess.CrashExit)
|
|
|
|
def process_finished(self, exit_code, exit_status):
|
|
debug_print(f"DEBUG: process_finished called with exit_code={exit_code}, exit_status={exit_status}")
|
|
# Reset button states
|
|
self.start_btn.setEnabled(True)
|
|
self.cancel_btn.setVisible(True)
|
|
self.cancel_install_btn.setVisible(False)
|
|
debug_print("DEBUG: Button states reset in process_finished")
|
|
|
|
|
|
if exit_code == 0:
|
|
# TTW installation complete
|
|
self._safe_append_text("\nTTW installation completed successfully!")
|
|
self._safe_append_text("The merged TTW files have been created in the output directory.")
|
|
|
|
# Check if we're in modlist integration mode
|
|
if self._integration_mode:
|
|
self._safe_append_text("\nIntegrating TTW into modlist...")
|
|
self._perform_modlist_integration()
|
|
else:
|
|
# Standard mode - ask user if they want to create a mod archive for MO2
|
|
reply = MessageService.question(
|
|
self, "TTW Installation Complete!",
|
|
"Tale of Two Wastelands installation completed successfully!\n\n"
|
|
f"Output location: {self.install_dir_edit.text()}\n\n"
|
|
"Would you like to create a zipped mod archive for MO2?\n"
|
|
"This will package the TTW files for easy installation into Mod Organizer 2.",
|
|
critical=False
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self._create_ttw_mod_archive()
|
|
else:
|
|
MessageService.information(
|
|
self, "Installation Complete",
|
|
"TTW installation complete!\n\n"
|
|
"You can manually use the TTW files from the output directory.",
|
|
safety_level="medium"
|
|
)
|
|
else:
|
|
# Check for user cancellation first
|
|
last_output = self.console.toPlainText()
|
|
if "cancelled by user" in last_output.lower():
|
|
MessageService.information(self, "Installation Cancelled", "The installation was cancelled by the user.", safety_level="low")
|
|
else:
|
|
MessageService.critical(self, "Install Failed", "The modlist install failed. Please check the console output for details.")
|
|
self._safe_append_text(f"\nInstall failed (exit code {exit_code}).")
|
|
self.console.moveCursor(QTextCursor.End)
|
|
|