Sync from development - prepare for v0.1.7

This commit is contained in:
Omni
2025-11-04 12:54:15 +00:00
parent 91ac08afb2
commit 9680814bbb
25 changed files with 4560 additions and 259 deletions

View File

@@ -909,18 +909,24 @@ class JackifyMainWindow(QMainWindow):
# Create screens using refactored codebase
from jackify.frontends.gui.screens import (
MainMenu, ModlistTasksScreen,
MainMenu, ModlistTasksScreen, AdditionalTasksScreen,
InstallModlistScreen, ConfigureNewModlistScreen, ConfigureExistingModlistScreen
)
from jackify.frontends.gui.screens.install_ttw import InstallTTWScreen
self.main_menu = MainMenu(stacked_widget=self.stacked_widget, dev_mode=dev_mode)
self.feature_placeholder = FeaturePlaceholder(stacked_widget=self.stacked_widget)
self.modlist_tasks_screen = ModlistTasksScreen(
stacked_widget=self.stacked_widget,
stacked_widget=self.stacked_widget,
main_menu_index=0,
dev_mode=dev_mode
)
self.additional_tasks_screen = AdditionalTasksScreen(
stacked_widget=self.stacked_widget,
main_menu_index=0,
system_info=self.system_info
)
self.install_modlist_screen = InstallModlistScreen(
stacked_widget=self.stacked_widget,
main_menu_index=0
@@ -933,14 +939,26 @@ class JackifyMainWindow(QMainWindow):
stacked_widget=self.stacked_widget,
main_menu_index=0
)
self.install_ttw_screen = InstallTTWScreen(
stacked_widget=self.stacked_widget,
main_menu_index=0,
system_info=self.system_info
)
# Let TTW screen request window resize for expand/collapse
try:
self.install_ttw_screen.resize_request.connect(self._on_child_resize_request)
except Exception:
pass
# Add screens to stacked widget
self.stacked_widget.addWidget(self.main_menu) # Index 0: Main Menu
self.stacked_widget.addWidget(self.feature_placeholder) # Index 1: Placeholder
self.stacked_widget.addWidget(self.modlist_tasks_screen) # Index 2: Modlist Tasks
self.stacked_widget.addWidget(self.install_modlist_screen) # Index 3: Install Modlist
self.stacked_widget.addWidget(self.configure_new_modlist_screen) # Index 4: Configure New
self.stacked_widget.addWidget(self.configure_existing_modlist_screen) # Index 5: Configure Existing
self.stacked_widget.addWidget(self.additional_tasks_screen) # Index 3: Additional Tasks
self.stacked_widget.addWidget(self.install_modlist_screen) # Index 4: Install Modlist
self.stacked_widget.addWidget(self.install_ttw_screen) # Index 5: Install TTW
self.stacked_widget.addWidget(self.configure_new_modlist_screen) # Index 6: Configure New
self.stacked_widget.addWidget(self.configure_existing_modlist_screen) # Index 7: Configure Existing
# Add debug tracking for screen changes
self.stacked_widget.currentChanged.connect(self._debug_screen_change)
@@ -1025,9 +1043,11 @@ class JackifyMainWindow(QMainWindow):
0: "Main Menu",
1: "Feature Placeholder",
2: "Modlist Tasks Menu",
3: "Install Modlist Screen",
4: "Configure New Modlist",
5: "Configure Existing Modlist"
3: "Additional Tasks Menu",
4: "Install Modlist Screen",
5: "Install TTW Screen",
6: "Configure New Modlist",
7: "Configure Existing Modlist"
}
screen_name = screen_names.get(index, f"Unknown Screen (Index {index})")
widget = self.stacked_widget.widget(index)
@@ -1180,6 +1200,80 @@ class JackifyMainWindow(QMainWindow):
import traceback
traceback.print_exc()
def _on_child_resize_request(self, mode: str):
debug_print(f"DEBUG: _on_child_resize_request called with mode='{mode}', current_size={self.size()}")
# On Steam Deck we keep the stable, full-size layout and ignore child resize
try:
if self.system_info and self.system_info.is_steamdeck:
debug_print("DEBUG: Steam Deck detected, ignoring resize request")
# Hide the checkbox if present (Deck uses full layout)
try:
if hasattr(self, 'install_ttw_screen') and self.install_ttw_screen.show_details_checkbox:
self.install_ttw_screen.show_details_checkbox.setVisible(False)
except Exception:
pass
return
except Exception:
pass
# Ensure we can actually resize
self.showNormal()
self.setMaximumHeight(16777215)
debug_print(f"DEBUG: Set max height to unlimited, current_size={self.size()}")
if mode == 'expand':
# Restore a sensible minimum and expand height
min_width = max(1200, self.minimumWidth())
min_height = 900
debug_print(f"DEBUG: Expand mode - min_width={min_width}, min_height={min_height}")
try:
from PySide6.QtCore import QSize
self.setMinimumSize(QSize(min_width, min_height))
except Exception:
self.setMinimumSize(min_width, min_height)
# Animate to target height
target_height = max(self.size().height(), min_height)
self._animate_height(target_height)
else:
# Collapse to compact height computed from the TTW screen's sizeHint
try:
content_hint = self.install_ttw_screen.sizeHint().height()
except Exception:
content_hint = 460
compact_height = max(440, min(560, content_hint + 20))
debug_print(f"DEBUG: Collapse mode - content_hint={content_hint}, compact_height={compact_height}")
from PySide6.QtCore import QSize
self.setMaximumHeight(compact_height)
self.setMinimumSize(QSize(max(1200, self.minimumWidth()), compact_height))
# Animate to compact height
self._animate_height(compact_height)
def _animate_height(self, target_height: int, duration_ms: int = 180):
"""Smoothly animate the window height to target_height.
Kept local imports to minimize global impact and avoid touching module headers.
"""
try:
from PySide6.QtCore import QEasingCurve, QPropertyAnimation, QRect
except Exception:
# Fallback to immediate resize if animation types are unavailable
before = self.size()
self.resize(self.size().width(), target_height)
debug_print(f"DEBUG: Animated fallback resize from {before} to {self.size()}")
return
# Build end rect with same x/y/width and target height
start_rect = self.geometry()
end_rect = QRect(start_rect.x(), start_rect.y(), start_rect.width(), target_height)
# Hold reference to avoid GC stopping the animation
self._resize_anim = QPropertyAnimation(self, b"geometry")
self._resize_anim.setDuration(duration_ms)
self._resize_anim.setEasingCurve(QEasingCurve.OutCubic)
self._resize_anim.setStartValue(start_rect)
self._resize_anim.setEndValue(end_rect)
self._resize_anim.start()
def resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):

View File

@@ -6,6 +6,7 @@ Contains all the GUI screen components for Jackify.
from .main_menu import MainMenu
from .modlist_tasks import ModlistTasksScreen
from .additional_tasks import AdditionalTasksScreen
from .install_modlist import InstallModlistScreen
from .configure_new_modlist import ConfigureNewModlistScreen
from .configure_existing_modlist import ConfigureExistingModlistScreen
@@ -13,6 +14,7 @@ from .configure_existing_modlist import ConfigureExistingModlistScreen
__all__ = [
'MainMenu',
'ModlistTasksScreen',
'AdditionalTasksScreen',
'InstallModlistScreen',
'ConfigureNewModlistScreen',
'ConfigureExistingModlistScreen'

View File

@@ -0,0 +1,169 @@
"""
Additional Tasks & Tools Screen
Simple screen for TTW automation only.
Follows the same pattern as ModlistTasksScreen.
"""
import logging
from typing import Optional
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGridLayout
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from jackify.backend.models.configuration import SystemInfo
from ..shared_theme import JACKIFY_COLOR_BLUE
logger = logging.getLogger(__name__)
class AdditionalTasksScreen(QWidget):
"""Simple Additional Tasks screen for TTW only"""
def __init__(self, stacked_widget=None, main_menu_index=0, system_info: Optional[SystemInfo] = None):
super().__init__()
self.stacked_widget = stacked_widget
self.main_menu_index = main_menu_index
self.system_info = system_info or SystemInfo(is_steamdeck=False)
self._setup_ui()
def _setup_ui(self):
"""Set up the user interface following ModlistTasksScreen pattern"""
layout = QVBoxLayout()
layout.setContentsMargins(40, 40, 40, 40)
layout.setSpacing(0)
# Header section
self._setup_header(layout)
# Menu buttons section
self._setup_menu_buttons(layout)
# Bottom spacer
layout.addStretch()
self.setLayout(layout)
def _setup_header(self, layout):
"""Set up the header section"""
header_layout = QVBoxLayout()
header_layout.setSpacing(0)
# Title
title = QLabel("<b>Additional Tasks & Tools</b>")
title.setStyleSheet(f"font-size: 24px; color: {JACKIFY_COLOR_BLUE};")
title.setAlignment(Qt.AlignHCenter)
header_layout.addWidget(title)
# Add a spacer to match main menu vertical spacing
header_layout.addSpacing(16)
# Description
desc = QLabel(
"TTW automation and additional tools.<br>&nbsp;"
)
desc.setWordWrap(True)
desc.setStyleSheet("color: #ccc;")
desc.setAlignment(Qt.AlignHCenter)
header_layout.addWidget(desc)
header_layout.addSpacing(24)
# Separator (shorter like main menu)
sep = QLabel()
sep.setFixedHeight(2)
sep.setFixedWidth(400) # Match button width
sep.setStyleSheet("background: #fff;")
header_layout.addWidget(sep, alignment=Qt.AlignHCenter)
header_layout.addSpacing(16)
layout.addLayout(header_layout)
def _setup_menu_buttons(self, layout):
"""Set up the menu buttons section"""
# Menu options - ONLY TTW and placeholder
MENU_ITEMS = [
("Install TTW", "ttw_install", "Install Tale of Two Wastelands using Hoolamike automation"),
("Coming Soon...", "coming_soon", "Additional tools will be added in future updates"),
("Return to Main Menu", "return_main_menu", "Go back to the main menu"),
]
# Create grid layout for buttons (mirror ModlistTasksScreen pattern)
button_grid = QGridLayout()
button_grid.setSpacing(16)
button_grid.setAlignment(Qt.AlignHCenter)
button_width = 400
button_height = 50
for i, (label, action_id, description) in enumerate(MENU_ITEMS):
# Create button
btn = QPushButton(label)
btn.setFixedSize(button_width, button_height)
btn.setStyleSheet(f"""
QPushButton {{
background-color: #4a5568;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
text-align: center;
}}
QPushButton:hover {{
background-color: #5a6578;
}}
QPushButton:pressed {{
background-color: {JACKIFY_COLOR_BLUE};
}}
""")
btn.clicked.connect(lambda checked, a=action_id: self._handle_button_click(a))
# Description label
desc_label = QLabel(description)
desc_label.setAlignment(Qt.AlignHCenter)
desc_label.setStyleSheet("color: #999; font-size: 12px;")
desc_label.setWordWrap(True)
desc_label.setFixedWidth(button_width)
# Add to grid (button row, then description row)
button_grid.addWidget(btn, i * 2, 0, Qt.AlignHCenter)
button_grid.addWidget(desc_label, i * 2 + 1, 0, Qt.AlignHCenter)
layout.addLayout(button_grid)
# Removed _create_menu_button; using same pattern as ModlistTasksScreen
def _handle_button_click(self, action_id):
"""Handle button clicks"""
if action_id == "ttw_install":
self._show_ttw_info()
elif action_id == "coming_soon":
self._show_coming_soon_info()
elif action_id == "return_main_menu":
self._return_to_main_menu()
def _show_ttw_info(self):
"""Navigate to TTW installation screen"""
if self.stacked_widget:
# Navigate to TTW installation screen (index 5)
self.stacked_widget.setCurrentIndex(5)
def _show_coming_soon_info(self):
"""Show coming soon info"""
from ..services.message_service import MessageService
MessageService.information(
self,
"Coming Soon",
"Additional tools and features will be added in future updates.\n\n"
"Check back later for more functionality!"
)
def _return_to_main_menu(self):
"""Return to main menu"""
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(self.main_menu_index)

View File

@@ -412,6 +412,9 @@ class ConfigureExistingModlistScreen(QWidget):
pass
def validate_and_start_configure(self):
# Reload config to pick up any settings changes made in Settings dialog
self.config_handler.reload_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
@@ -453,10 +456,14 @@ class ConfigureExistingModlistScreen(QWidget):
def start_workflow(self, modlist_name, install_dir, resolution):
"""Start the configuration workflow using backend service directly"""
# CRITICAL: Reload config from disk to pick up any settings changes from Settings dialog
# This ensures Proton version and winetricks settings are current
self.config_handler._load_config()
try:
# Start time tracking
self._workflow_start_time = time.time()
self._safe_append_text("[Jackify] Starting post-install configuration...")
# Create configuration thread using backend service

View File

@@ -554,6 +554,9 @@ class ConfigureNewModlistScreen(QWidget):
return True # Continue anyway
def validate_and_start_configure(self):
# Reload config to pick up any settings changes made in Settings dialog
self.config_handler.reload_config()
# Check protontricks before proceeding
if not self._check_protontricks():
return
@@ -665,6 +668,10 @@ class ConfigureNewModlistScreen(QWidget):
MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.", safety_level="medium")
def configure_modlist(self):
# CRITICAL: Reload config from disk to pick up any settings changes from Settings dialog
# This ensures Proton version and winetricks settings are current
self.config_handler._load_config()
install_dir = os.path.dirname(self.install_dir_edit.text().strip()) if self.install_dir_edit.text().strip().endswith('ModOrganizer.exe') else self.install_dir_edit.text().strip()
modlist_name = self.modlist_name_edit.text().strip()
mo2_exe_path = self.install_dir_edit.text().strip()
@@ -672,12 +679,12 @@ class ConfigureNewModlistScreen(QWidget):
if not install_dir or not modlist_name:
MessageService.warning(self, "Missing Info", "Install directory or modlist name is missing.", safety_level="low")
return
# Use automated prefix service instead of manual steps
self._safe_append_text("")
self._safe_append_text("=== Steam Integration Phase ===")
self._safe_append_text("Starting automated Steam setup workflow...")
# Start automated prefix workflow
self._start_automated_prefix_workflow(modlist_name, install_dir, mo2_exe_path, resolution)

View File

@@ -396,7 +396,7 @@ class InstallModlistScreen(QWidget):
header_layout.addWidget(title)
# Description
desc = QLabel(
"This screen allows you to install a Wabbajack modlist using Jackify's native Linux tools. "
"This screen allows you to install a Wabbajack modlist using Jackify. "
"Configure your options and start the installation."
)
desc.setWordWrap(True)
@@ -1072,7 +1072,8 @@ class InstallModlistScreen(QWidget):
line_lower = line.lower()
if (
("jackify-engine" in line_lower or "7zz" in line_lower or "texconv" in line_lower or
"wine" in line_lower or "wine64" in line_lower or "protontricks" in line_lower)
"wine" in line_lower or "wine64" in line_lower or "protontricks" in line_lower or
"hoolamike" in line_lower)
and "jackify-gui.py" not in line_lower
):
cols = line.strip().split(None, 3)
@@ -1091,29 +1092,198 @@ class InstallModlistScreen(QWidget):
"""Check if protontricks is available before critical operations"""
try:
is_installed, installation_type, details = self.protontricks_service.detect_protontricks()
if not is_installed:
# Show protontricks error dialog
from jackify.frontends.gui.dialogs.protontricks_error_dialog import ProtontricksErrorDialog
dialog = ProtontricksErrorDialog(self.protontricks_service, self)
result = dialog.exec()
if result == QDialog.Rejected:
return False
# Re-check after dialog
is_installed, _, _ = self.protontricks_service.detect_protontricks(use_cache=False)
return is_installed
return True
except Exception as e:
print(f"Error checking protontricks: {e}")
MessageService.warning(self, "Protontricks Check Failed",
MessageService.warning(self, "Protontricks Check Failed",
f"Unable to verify protontricks installation: {e}\n\n"
"Continuing anyway, but some features may not work correctly.")
return True # Continue anyway
def _check_ttw_eligibility(self, modlist_name: str, game_type: str, install_dir: str) -> bool:
"""Check if modlist is FNV, TTW-compatible, and doesn't already have TTW
Args:
modlist_name: Name of the installed modlist
game_type: Game type (e.g., 'falloutnv')
install_dir: Modlist installation directory
Returns:
bool: True if should offer TTW integration
"""
try:
# Check 1: Must be Fallout New Vegas
if game_type.lower() not in ['falloutnv', 'fallout new vegas', 'fallout_new_vegas']:
return False
# Check 2: Must be on whitelist
from jackify.backend.data.ttw_compatible_modlists import is_ttw_compatible
if not is_ttw_compatible(modlist_name):
return False
# Check 3: TTW must not already be installed
if self._detect_existing_ttw(install_dir):
debug_print("DEBUG: TTW already installed, skipping prompt")
return False
return True
except Exception as e:
debug_print(f"DEBUG: Error checking TTW eligibility: {e}")
return False
def _detect_existing_ttw(self, install_dir: str) -> bool:
"""Check if TTW is already installed in the modlist
Args:
install_dir: Modlist installation directory
Returns:
bool: True if TTW is already present
"""
try:
from pathlib import Path
mods_dir = Path(install_dir) / "mods"
if not mods_dir.exists():
return False
# Check for folders containing "Tale of Two Wastelands" that have actual TTW content
# Exclude separators and placeholder folders
for folder in mods_dir.iterdir():
if not folder.is_dir():
continue
folder_name_lower = folder.name.lower()
# Skip separator folders and placeholders
if "_separator" in folder_name_lower or "put" in folder_name_lower or "here" in folder_name_lower:
continue
# Check if folder name contains TTW indicator
if "tale of two wastelands" in folder_name_lower:
# Verify it has actual TTW content by checking for the main ESM
ttw_esm = folder / "TaleOfTwoWastelands.esm"
if ttw_esm.exists():
debug_print(f"DEBUG: Found existing TTW installation: {folder.name}")
return True
else:
debug_print(f"DEBUG: Found TTW folder but no ESM, skipping: {folder.name}")
return False
except Exception as e:
debug_print(f"DEBUG: Error detecting existing TTW: {e}")
return False # Assume not installed on error
def _initiate_ttw_workflow(self, modlist_name: str, install_dir: str):
"""Navigate to TTW screen and set it up for modlist integration
Args:
modlist_name: Name of the modlist that needs TTW integration
install_dir: Path to the modlist installation directory
"""
try:
# Store modlist context for later use when TTW completes
self._ttw_modlist_name = modlist_name
self._ttw_install_dir = install_dir
# Get reference to TTW screen BEFORE navigation
if self.stacked_widget:
ttw_screen = self.stacked_widget.widget(5)
# Set integration mode BEFORE navigating to avoid showEvent race condition
if hasattr(ttw_screen, 'set_modlist_integration_mode'):
ttw_screen.set_modlist_integration_mode(modlist_name, install_dir)
# Connect to completion signal to show success dialog after TTW
if hasattr(ttw_screen, 'integration_complete'):
ttw_screen.integration_complete.connect(self._on_ttw_integration_complete)
else:
debug_print("WARNING: TTW screen does not support modlist integration mode yet")
# Navigate to TTW screen AFTER setting integration mode
self.stacked_widget.setCurrentIndex(5)
# Force collapsed state shortly after navigation to avoid any
# showEvent/layout timing races that may leave it expanded
try:
from PySide6.QtCore import QTimer
QTimer.singleShot(50, lambda: getattr(ttw_screen, 'force_collapsed_state', lambda: None)())
except Exception:
pass
except Exception as e:
debug_print(f"ERROR: Failed to initiate TTW workflow: {e}")
MessageService.critical(
self,
"TTW Navigation Failed",
f"Failed to navigate to TTW installation screen: {str(e)}"
)
def _on_ttw_integration_complete(self, success: bool, ttw_version: str = ""):
"""Handle completion of TTW integration and show final success dialog
Args:
success: Whether TTW integration completed successfully
ttw_version: Version of TTW that was installed
"""
try:
if not success:
MessageService.critical(
self,
"TTW Integration Failed",
"Tale of Two Wastelands integration did not complete successfully."
)
return
# Navigate back to this screen to show success dialog
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(4)
# Build success message including TTW installation
modlist_name = getattr(self, '_ttw_modlist_name', 'Unknown')
time_str = getattr(self, '_elapsed_time_str', '0m 0s')
game_name = "Fallout New Vegas"
# Show enhanced success dialog
success_dialog = SuccessDialog(
modlist_name=modlist_name,
workflow_type="install",
time_taken=time_str,
game_name=game_name,
parent=self
)
# Add TTW installation info to dialog if possible
if hasattr(success_dialog, 'add_info_line'):
success_dialog.add_info_line(f"TTW {ttw_version} integrated successfully")
success_dialog.show()
except Exception as e:
debug_print(f"ERROR: Failed to show final success dialog: {e}")
MessageService.critical(
self,
"Display Error",
f"TTW integration completed but failed to show success dialog: {str(e)}"
)
def _on_api_key_save_toggled(self, checked):
"""Handle immediate API key saving with silent validation when checkbox is toggled"""
try:
@@ -1188,11 +1358,14 @@ class InstallModlistScreen(QWidget):
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()
# Check protontricks before proceeding
if not self._check_protontricks():
return
# Disable all controls during installation (except Cancel)
self._disable_controls_during_operation()
@@ -1764,6 +1937,11 @@ class InstallModlistScreen(QWidget):
MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.")
def start_automated_prefix_workflow(self):
"""Start the automated prefix creation workflow"""
# CRITICAL: Reload config from disk to pick up any settings changes from Settings dialog
# This ensures Proton version and winetricks settings are current
self.config_handler._load_config()
# Ensure _current_resolution is always set before starting workflow
if not hasattr(self, '_current_resolution') or self._current_resolution is None:
resolution = self.resolution_combo.currentText() if hasattr(self, 'resolution_combo') else None
@@ -1775,7 +1953,7 @@ class InstallModlistScreen(QWidget):
self._current_resolution = resolution
else:
self._current_resolution = None
"""Start the automated prefix creation workflow"""
try:
# Disable controls during installation
self._disable_controls_during_operation()
@@ -2002,6 +2180,31 @@ class InstallModlistScreen(QWidget):
'enderal': 'Enderal'
}
game_name = display_names.get(self._current_game_type, self._current_game_name)
# Check for TTW eligibility before showing final success dialog
install_dir = self.install_dir_edit.text().strip()
if self._check_ttw_eligibility(modlist_name, self._current_game_type, install_dir):
# Offer TTW installation
reply = MessageService.question(
self,
"Install TTW?",
f"{modlist_name} requires Tale of Two Wastelands!\n\n"
"Would you like to install and configure TTW automatically now?\n\n"
"This will:\n"
"• Guide you through TTW installation\n"
"• Automatically integrate TTW into your modlist\n"
"• Configure load order correctly\n\n"
"Note: TTW installation can take a while. You can also install TTW later from Additional Tasks & Tools.",
critical=False,
safety_level="medium"
)
if reply == QMessageBox.Yes:
# Navigate to TTW screen
self._initiate_ttw_workflow(modlist_name, install_dir)
return # Don't show success dialog yet, will show after TTW completes
# Show normal success dialog
success_dialog = SuccessDialog(
modlist_name=modlist_name,
workflow_type="install",
@@ -2747,7 +2950,4 @@ https://wiki.scenicroute.games/Somnium/1_Installation.html</i>"""
# 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()
event.accept()

File diff suppressed because it is too large Load Diff

View File

@@ -47,12 +47,9 @@ class MainMenu(QWidget):
button_height = 60
MENU_ITEMS = [
("Modlist Tasks", "modlist_tasks", "Manage your modlists with native Linux tools"),
("Coming Soon...", "coming_soon", "More features coming soon!"),
("Additional Tasks", "additional_tasks", "Additional Tasks & Tools, such as TTW Installation"),
("Exit Jackify", "exit_jackify", "Close the application"),
]
if self.dev_mode:
MENU_ITEMS.append(("Hoolamike Tasks", "hoolamike_tasks", "Manage Hoolamike modding tools"))
MENU_ITEMS.append(("Additional Tasks", "additional_tasks", "Additional utilities and tools"))
MENU_ITEMS.append(("Exit Jackify", "exit_jackify", "Close the application"))
for label, action_id, description in MENU_ITEMS:
# Main button
@@ -121,8 +118,10 @@ class MainMenu(QWidget):
msg.exec()
elif action_id == "modlist_tasks" and self.stacked_widget:
self.stacked_widget.setCurrentIndex(2)
elif action_id == "additional_tasks" and self.stacked_widget:
self.stacked_widget.setCurrentIndex(3)
elif action_id == "return_main_menu":
# This is the main menu, so do nothing
pass
elif self.stacked_widget:
self.stacked_widget.setCurrentIndex(2) # Placeholder for now
self.stacked_widget.setCurrentIndex(1) # Default to placeholder

View File

@@ -198,11 +198,11 @@ class ModlistTasksScreen(QWidget):
if action_id == "return_main_menu":
self.stacked_widget.setCurrentIndex(0)
elif action_id == "install_modlist":
self.stacked_widget.setCurrentIndex(3)
self.stacked_widget.setCurrentIndex(4) # Install Modlist Screen
elif action_id == "configure_new_modlist":
self.stacked_widget.setCurrentIndex(4)
self.stacked_widget.setCurrentIndex(6) # Configure New Modlist Screen
elif action_id == "configure_existing_modlist":
self.stacked_widget.setCurrentIndex(5)
self.stacked_widget.setCurrentIndex(7) # Configure Existing Modlist Screen
def go_back(self):
"""Return to main menu"""

View File

@@ -220,6 +220,8 @@ class MessageService:
msg_box = MessageService._create_base_message_box(parent, critical, safety_level)
msg_box.setIcon(QMessageBox.Information)
msg_box.setWindowTitle(title)
msg_box.setTextFormat(Qt.RichText)
msg_box.setTextInteractionFlags(Qt.TextBrowserInteraction)
msg_box.setText(message)
msg_box.setStandardButtons(buttons)
msg_box.setDefaultButton(default_button)

View File

@@ -9,6 +9,21 @@ ANSI_COLOR_MAP = {
}
ANSI_RE = re.compile(r'\x1b\[(\d+)(;\d+)?m')
# Pattern to match terminal control codes (cursor movement, line clearing, etc.)
ANSI_CONTROL_RE = re.compile(
r'\x1b\[' # CSI sequence start
r'[0-9;]*' # Parameters
r'[A-Za-z]' # Command letter
)
def strip_ansi_control_codes(text):
"""Remove ALL ANSI escape sequences including control codes.
This is useful for Hoolamike output which uses terminal control codes
for progress bars that don't render well in QTextEdit.
"""
return ANSI_CONTROL_RE.sub('', text)
def ansi_to_html(text):
"""Convert ANSI color codes to HTML"""
result = ''