Sync from development - prepare for v0.1.6

This commit is contained in:
Omni
2025-10-16 14:44:49 +01:00
parent 7212a58480
commit 430d085287
59 changed files with 939 additions and 452 deletions

View File

@@ -303,29 +303,50 @@ class SettingsDialog(QDialog):
general_layout.addWidget(api_group)
general_layout.addSpacing(12)
# --- Default Proton Version Section ---
proton_group = QGroupBox("Default Proton Version")
# --- Proton Version Settings Section ---
proton_group = QGroupBox("Proton Version Settings")
proton_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
proton_layout = QHBoxLayout()
proton_layout = QVBoxLayout()
proton_group.setLayout(proton_layout)
self.proton_dropdown = QComboBox()
self.proton_dropdown.setToolTip("Select default Proton version for shortcut creation and texture processing")
self.proton_dropdown.setMinimumWidth(200)
# Install Proton Version (for jackify-engine texture processing)
install_proton_layout = QHBoxLayout()
self.install_proton_dropdown = QComboBox()
self.install_proton_dropdown.setToolTip("Proton version for modlist installation and texture processing (requires fast Proton)")
self.install_proton_dropdown.setMinimumWidth(200)
# Populate Proton dropdown
self._populate_proton_dropdown()
install_refresh_btn = QPushButton("")
install_refresh_btn.setFixedSize(30, 30)
install_refresh_btn.setToolTip("Refresh install Proton version list")
install_refresh_btn.clicked.connect(self._refresh_install_proton_dropdown)
# Refresh button for Proton detection
refresh_btn = QPushButton("")
refresh_btn.setFixedSize(30, 30)
refresh_btn.setToolTip("Refresh Proton version list")
refresh_btn.clicked.connect(self._refresh_proton_dropdown)
install_proton_layout.addWidget(QLabel("Install Proton:"))
install_proton_layout.addWidget(self.install_proton_dropdown)
install_proton_layout.addWidget(install_refresh_btn)
install_proton_layout.addStretch()
proton_layout.addWidget(QLabel("Proton Version:"))
proton_layout.addWidget(self.proton_dropdown)
proton_layout.addWidget(refresh_btn)
proton_layout.addStretch()
# Game Proton Version (for game shortcuts)
game_proton_layout = QHBoxLayout()
self.game_proton_dropdown = QComboBox()
self.game_proton_dropdown.setToolTip("Proton version for game shortcuts (can be any Proton 9+)")
self.game_proton_dropdown.setMinimumWidth(200)
game_refresh_btn = QPushButton("")
game_refresh_btn.setFixedSize(30, 30)
game_refresh_btn.setToolTip("Refresh game Proton version list")
game_refresh_btn.clicked.connect(self._refresh_game_proton_dropdown)
game_proton_layout.addWidget(QLabel("Game Proton:"))
game_proton_layout.addWidget(self.game_proton_dropdown)
game_proton_layout.addWidget(game_refresh_btn)
game_proton_layout.addStretch()
proton_layout.addLayout(install_proton_layout)
proton_layout.addLayout(game_proton_layout)
# Populate both Proton dropdowns
self._populate_install_proton_dropdown()
self._populate_game_proton_dropdown()
general_layout.addWidget(proton_group)
general_layout.addSpacing(12)
@@ -416,6 +437,22 @@ class SettingsDialog(QDialog):
self.bandwidth_spin = None # No bandwidth UI if Downloads resource doesn't exist
advanced_layout.addWidget(resource_group)
# Component Installation Method Section
component_group = QGroupBox("Component Installation")
component_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
component_layout = QVBoxLayout()
component_group.setLayout(component_layout)
self.use_winetricks_checkbox = QCheckBox("Use winetricks for component installation (faster)")
self.use_winetricks_checkbox.setChecked(self.config_handler.get('use_winetricks_for_components', True))
self.use_winetricks_checkbox.setToolTip(
"When enabled: Uses winetricks for most components (faster) and protontricks only for dotnet40 (more reliable).\n"
"When disabled: Uses protontricks for all components (legacy behavior, slower but more compatible)."
)
component_layout.addWidget(self.use_winetricks_checkbox)
advanced_layout.addWidget(component_group)
advanced_layout.addStretch() # Add stretch to push content to top
self.tab_widget.addTab(advanced_tab, "Advanced")
@@ -488,167 +525,250 @@ class SettingsDialog(QDialog):
except:
return 'auto'
def _populate_proton_dropdown(self):
"""Populate Proton version dropdown with detected versions (includes GE-Proton and Valve Proton)"""
def _populate_install_proton_dropdown(self):
"""Populate Install Proton dropdown (Experimental/GE-Proton 10+ only for fast texture processing)"""
try:
from jackify.backend.handlers.wine_utils import WineUtils
# Get all available Proton versions (GE-Proton + Valve Proton)
# Get all available Proton versions
available_protons = WineUtils.scan_all_proton_versions()
# Add "Auto" option first
self.proton_dropdown.addItem("Auto", "auto")
self.install_proton_dropdown.addItem("Auto (Recommended)", "auto")
# Filter for fast Proton versions only
fast_protons = []
slow_protons = []
# Add detected Proton versions with type indicators
for proton in available_protons:
proton_name = proton.get('name', 'Unknown Proton')
proton_type = proton.get('type', 'Unknown')
# Format display name to show type for clarity
is_fast_proton = False
# Fast Protons: Experimental, GE-Proton 10+
if proton_name == "Proton - Experimental":
is_fast_proton = True
elif proton_type == 'GE-Proton':
# For GE-Proton, check major_version field
major_version = proton.get('major_version', 0)
if major_version >= 10:
is_fast_proton = True
if is_fast_proton:
if proton_type == 'GE-Proton':
display_name = f"{proton_name} (GE)"
else:
display_name = proton_name
fast_protons.append((display_name, str(proton['path'])))
else:
# Slow Protons: Valve 9, 10 beta, older GE-Proton, etc.
if proton_type == 'GE-Proton':
display_name = f"{proton_name} (GE) (Slow texture processing)"
else:
display_name = f"{proton_name} (Slow texture processing)"
slow_protons.append((display_name, str(proton['path'])))
# Add fast Protons first
for display_name, path in fast_protons:
self.install_proton_dropdown.addItem(display_name, path)
# Add separator and slow Protons with warnings
if slow_protons:
self.install_proton_dropdown.insertSeparator(self.install_proton_dropdown.count())
for display_name, path in slow_protons:
self.install_proton_dropdown.addItem(display_name, path)
# Load saved preference
saved_proton = self.config_handler.get('proton_path', self._get_proton_10_path())
self._set_dropdown_selection(self.install_proton_dropdown, saved_proton)
except Exception as e:
logger.error(f"Failed to populate install Proton dropdown: {e}")
self.install_proton_dropdown.addItem("Auto (Recommended)", "auto")
def _populate_game_proton_dropdown(self):
"""Populate Game Proton dropdown (any Proton 9+ for game compatibility)"""
try:
from jackify.backend.handlers.wine_utils import WineUtils
# Get all available Proton versions
available_protons = WineUtils.scan_all_proton_versions()
# Add "Same as Install" option first
self.game_proton_dropdown.addItem("Same as Install Proton", "same_as_install")
# Add all Proton 9+ versions
for proton in available_protons:
proton_name = proton.get('name', 'Unknown Proton')
proton_type = proton.get('type', 'Unknown')
# Add type indicator for clarity
if proton_type == 'GE-Proton':
display_name = f"{proton_name} (GE)"
elif proton_type == 'Valve-Proton':
display_name = f"{proton_name}"
else:
display_name = proton_name
self.proton_dropdown.addItem(display_name, str(proton['path']))
self.game_proton_dropdown.addItem(display_name, str(proton['path']))
# Load saved preference and determine UI selection
saved_proton = self.config_handler.get('proton_path', self._get_proton_10_path())
# Check if saved path matches any specific Proton in dropdown
found_match = False
for i in range(self.proton_dropdown.count()):
if self.proton_dropdown.itemData(i) == saved_proton:
self.proton_dropdown.setCurrentIndex(i)
found_match = True
break
# If no exact match found, check if it's a resolved auto-selection
if not found_match and saved_proton != "auto":
# This means config has a resolved path from previous "Auto" selection
# Show "Auto" in UI since user chose auto-detection
for i in range(self.proton_dropdown.count()):
if self.proton_dropdown.itemData(i) == "auto":
self.proton_dropdown.setCurrentIndex(i)
break
# Load saved preference
saved_game_proton = self.config_handler.get('game_proton_path', 'same_as_install')
self._set_dropdown_selection(self.game_proton_dropdown, saved_game_proton)
except Exception as e:
logger.error(f"Failed to populate Proton dropdown: {e}")
# Fallback: just show auto
self.proton_dropdown.addItem("Auto", "auto")
logger.error(f"Failed to populate game Proton dropdown: {e}")
self.game_proton_dropdown.addItem("Same as Install Proton", "same_as_install")
def _refresh_proton_dropdown(self):
"""Refresh Proton dropdown with latest detected versions"""
current_selection = self.proton_dropdown.currentData()
self.proton_dropdown.clear()
self._populate_proton_dropdown()
# Restore selection if still available
for i in range(self.proton_dropdown.count()):
if self.proton_dropdown.itemData(i) == current_selection:
self.proton_dropdown.setCurrentIndex(i)
def _set_dropdown_selection(self, dropdown, saved_value):
"""Helper to set dropdown selection based on saved value"""
found_match = False
for i in range(dropdown.count()):
if dropdown.itemData(i) == saved_value:
dropdown.setCurrentIndex(i)
found_match = True
break
# If no exact match and not auto/same_as_install, select first option
if not found_match and saved_value not in ["auto", "same_as_install"]:
dropdown.setCurrentIndex(0)
def _refresh_install_proton_dropdown(self):
"""Refresh Install Proton dropdown"""
current_selection = self.install_proton_dropdown.currentData()
self.install_proton_dropdown.clear()
self._populate_install_proton_dropdown()
self._set_dropdown_selection(self.install_proton_dropdown, current_selection)
def _refresh_game_proton_dropdown(self):
"""Refresh Game Proton dropdown"""
current_selection = self.game_proton_dropdown.currentData()
self.game_proton_dropdown.clear()
self._populate_game_proton_dropdown()
self._set_dropdown_selection(self.game_proton_dropdown, current_selection)
def _save(self):
# Validate values
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
if max_tasks_spin.value() > 128:
self.error_label.setText(f"Invalid value for {k}: Max Tasks must be <= 128.")
try:
# Validate values (only if resource_edits exist)
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
if max_tasks_spin.value() > 128:
self.error_label.setText(f"Invalid value for {k}: Max Tasks must be <= 128.")
return
if self.bandwidth_spin and self.bandwidth_spin.value() > 1000000:
self.error_label.setText("Bandwidth limit must be <= 1,000,000 KB/s.")
return
if self.bandwidth_spin and self.bandwidth_spin.value() > 1000000:
self.error_label.setText("Bandwidth limit must be <= 1,000,000 KB/s.")
return
self.error_label.setText("")
# Save resource settings
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
resource_data = self.resource_settings.get(k, {})
resource_data['MaxTasks'] = max_tasks_spin.value()
self.resource_settings[k] = resource_data
# Save bandwidth limit to Downloads resource MaxThroughput (only if bandwidth UI exists)
if self.bandwidth_spin:
if "Downloads" not in self.resource_settings:
self.resource_settings["Downloads"] = {"MaxTasks": 16} # Provide default MaxTasks
self.resource_settings["Downloads"]["MaxThroughput"] = self.bandwidth_spin.value()
# Save all resource settings (including bandwidth) in one operation
self._save_json(self.resource_settings_path, self.resource_settings)
# Save debug mode to config
self.config_handler.set('debug_mode', self.debug_checkbox.isChecked())
# Save API key
api_key = self.api_key_edit.text().strip()
self.config_handler.save_api_key(api_key)
# Save modlist base dirs
self.config_handler.set("modlist_install_base_dir", self.install_dir_edit.text().strip())
self.config_handler.set("modlist_downloads_base_dir", self.download_dir_edit.text().strip())
# Save jackify data directory (always store actual path, never None)
jackify_data_dir = self.jackify_data_dir_edit.text().strip()
self.config_handler.set("jackify_data_dir", jackify_data_dir)
self.error_label.setText("")
# Save Proton selection - resolve "auto" to actual path
selected_proton_path = self.proton_dropdown.currentData()
if selected_proton_path == "auto":
# Resolve "auto" to actual best Proton path using unified detection
try:
from jackify.backend.handlers.wine_utils import WineUtils
best_proton = WineUtils.select_best_proton()
# Save resource settings
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
resource_data = self.resource_settings.get(k, {})
resource_data['MaxTasks'] = max_tasks_spin.value()
self.resource_settings[k] = resource_data
if best_proton:
resolved_path = str(best_proton['path'])
resolved_version = best_proton['name']
else:
resolved_path = "auto"
resolved_version = "auto"
except:
resolved_path = "auto"
resolved_version = "auto"
else:
# User selected specific Proton version
resolved_path = selected_proton_path
# Extract version from dropdown text
resolved_version = self.proton_dropdown.currentText()
# Save bandwidth limit to Downloads resource MaxThroughput (only if bandwidth UI exists)
if self.bandwidth_spin:
if "Downloads" not in self.resource_settings:
self.resource_settings["Downloads"] = {"MaxTasks": 16} # Provide default MaxTasks
self.resource_settings["Downloads"]["MaxThroughput"] = self.bandwidth_spin.value()
self.config_handler.set("proton_path", resolved_path)
self.config_handler.set("proton_version", resolved_version)
# Save all resource settings (including bandwidth) in one operation
self._save_json(self.resource_settings_path, self.resource_settings)
# Force immediate save and verify
save_result = self.config_handler.save_config()
if not save_result:
self.logger.error("Failed to save Proton configuration")
else:
self.logger.info(f"Saved Proton config: path={resolved_path}, version={resolved_version}")
# Verify the save worked by reading it back
saved_path = self.config_handler.get("proton_path")
if saved_path != resolved_path:
self.logger.error(f"Config save verification failed: expected {resolved_path}, got {saved_path}")
# Save debug mode to config
self.config_handler.set('debug_mode', self.debug_checkbox.isChecked())
# Save API key
api_key = self.api_key_edit.text().strip()
self.config_handler.save_api_key(api_key)
# Save modlist base dirs
self.config_handler.set("modlist_install_base_dir", self.install_dir_edit.text().strip())
self.config_handler.set("modlist_downloads_base_dir", self.download_dir_edit.text().strip())
# Save jackify data directory (always store actual path, never None)
jackify_data_dir = self.jackify_data_dir_edit.text().strip()
self.config_handler.set("jackify_data_dir", jackify_data_dir)
# Save Install Proton selection - resolve "auto" to actual path
selected_install_proton_path = self.install_proton_dropdown.currentData()
if selected_install_proton_path == "auto":
# Resolve "auto" to actual best Proton path using unified detection
try:
from jackify.backend.handlers.wine_utils import WineUtils
best_proton = WineUtils.select_best_proton()
if best_proton:
resolved_install_path = str(best_proton['path'])
resolved_install_version = best_proton['name']
else:
resolved_install_path = "auto"
resolved_install_version = "auto"
except:
resolved_install_path = "auto"
resolved_install_version = "auto"
else:
self.logger.debug("Config save verified successfully")
# Refresh cached paths in GUI screens if Jackify directory changed
self._refresh_gui_paths()
# Check if debug mode changed and prompt for restart
new_debug_mode = self.debug_checkbox.isChecked()
if new_debug_mode != self._original_debug_mode:
reply = MessageService.question(self, "Restart Required", "Debug mode change requires a restart. Restart Jackify now?", safety_level="low")
if reply == QMessageBox.Yes:
import os, sys
# User requested restart - do it regardless of execution environment
self.accept()
# User selected specific Proton version
resolved_install_path = selected_install_proton_path
# Extract version from dropdown text
resolved_install_version = self.install_proton_dropdown.currentText()
# Check if running from AppImage
if os.environ.get('APPIMAGE'):
# AppImage: restart the AppImage
os.execv(os.environ['APPIMAGE'], [os.environ['APPIMAGE']] + sys.argv[1:])
self.config_handler.set("proton_path", resolved_install_path)
self.config_handler.set("proton_version", resolved_install_version)
# Save Game Proton selection
selected_game_proton_path = self.game_proton_dropdown.currentData()
if selected_game_proton_path == "same_as_install":
# Use same as install proton
resolved_game_path = resolved_install_path
resolved_game_version = resolved_install_version
else:
# User selected specific game Proton version
resolved_game_path = selected_game_proton_path
resolved_game_version = self.game_proton_dropdown.currentText()
self.config_handler.set("game_proton_path", resolved_game_path)
self.config_handler.set("game_proton_version", resolved_game_version)
# Save component installation method preference
self.config_handler.set("use_winetricks_for_components", self.use_winetricks_checkbox.isChecked())
# Force immediate save and verify
save_result = self.config_handler.save_config()
if not save_result:
self.logger.error("Failed to save Proton configuration")
else:
self.logger.info(f"Saved Proton config: install_path={resolved_install_path}, game_path={resolved_game_path}")
# Verify the save worked by reading it back
saved_path = self.config_handler.get("proton_path")
if saved_path != resolved_install_path:
self.logger.error(f"Config save verification failed: expected {resolved_install_path}, got {saved_path}")
else:
# Dev mode: restart the Python module
os.execv(sys.executable, [sys.executable, '-m', 'jackify.frontends.gui'] + sys.argv[1:])
return
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
self.accept()
self.logger.debug("Config save verified successfully")
# Refresh cached paths in GUI screens if Jackify directory changed
self._refresh_gui_paths()
# Check if debug mode changed and prompt for restart
new_debug_mode = self.debug_checkbox.isChecked()
if new_debug_mode != self._original_debug_mode:
reply = MessageService.question(self, "Restart Required", "Debug mode change requires a restart. Restart Jackify now?", safety_level="low")
if reply == QMessageBox.Yes:
import os, sys
# User requested restart - do it regardless of execution environment
self.accept()
# Check if running from AppImage
if os.environ.get('APPIMAGE'):
# AppImage: restart the AppImage
os.execv(os.environ['APPIMAGE'], [os.environ['APPIMAGE']] + sys.argv[1:])
else:
# Dev mode: restart the Python module
os.execv(sys.executable, [sys.executable, '-m', 'jackify.frontends.gui'] + sys.argv[1:])
return
# If we get here, no restart was needed
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
self.accept()
except Exception as e:
self.logger.error(f"Error saving settings: {e}")
MessageService.warning(self, "Save Error", f"Failed to save settings: {e}", safety_level="medium")
def _refresh_gui_paths(self):
"""Refresh cached paths in all GUI screens."""