mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:07:45 +02:00
Sync from development - prepare for v0.3.0
This commit is contained in:
386
jackify/frontends/gui/dialogs/settings_dialog.py
Normal file
386
jackify/frontends/gui/dialogs/settings_dialog.py
Normal file
@@ -0,0 +1,386 @@
|
||||
"""Settings dialog for Jackify GUI."""
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QCheckBox,
|
||||
QTabWidget, QFileDialog, QMessageBox, QProgressDialog, QApplication, QToolButton
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
from .settings_dialog_tabs import SettingsDialogTabsMixin
|
||||
from .settings_dialog_proton import SettingsDialogProtonMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsDialog(SettingsDialogTabsMixin, SettingsDialogProtonMixin, QDialog):
|
||||
def __init__(self, parent=None):
|
||||
try:
|
||||
super().__init__(parent)
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
import logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.config_handler = ConfigHandler()
|
||||
self._original_debug_mode = self.config_handler.get('debug_mode', False)
|
||||
self.setWindowTitle("Settings")
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(650)
|
||||
self.setMaximumWidth(800)
|
||||
self.setStyleSheet("QDialog { background-color: #232323; color: #eee; } QPushButton:hover { background-color: #333; }")
|
||||
|
||||
main_layout = QVBoxLayout()
|
||||
self.setLayout(main_layout)
|
||||
|
||||
# Create tab widget
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet("""
|
||||
QTabWidget::pane { border: 1px solid #555; background: #232323; }
|
||||
QTabBar::tab { background: #333; color: #eee; padding: 8px 16px; margin: 2px; }
|
||||
QTabBar::tab:selected { background: #555; }
|
||||
QTabBar::tab:hover { background: #444; }
|
||||
""")
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
# Create tabs
|
||||
self._create_general_tab()
|
||||
self._create_advanced_tab()
|
||||
|
||||
# --- Save/Close/Help Buttons ---
|
||||
btn_layout = QHBoxLayout()
|
||||
self.help_btn = QPushButton("Help")
|
||||
self.help_btn.setToolTip("Help/documentation coming soon!")
|
||||
self.help_btn.clicked.connect(self._show_help)
|
||||
btn_layout.addWidget(self.help_btn)
|
||||
btn_layout.addStretch(1)
|
||||
save_btn = QPushButton("Save")
|
||||
close_btn = QPushButton("Close")
|
||||
save_btn.clicked.connect(self._save)
|
||||
close_btn.clicked.connect(self.reject)
|
||||
btn_layout.addWidget(save_btn)
|
||||
btn_layout.addWidget(close_btn)
|
||||
|
||||
# Add error label for validation messages
|
||||
self.error_label = QLabel("")
|
||||
self.error_label.setStyleSheet("QLabel { color: #ff6b6b; }")
|
||||
main_layout.addWidget(self.error_label)
|
||||
|
||||
main_layout.addSpacing(10)
|
||||
main_layout.addLayout(btn_layout)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception in SettingsDialog.__init__: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _toggle_api_key_visibility(self, checked):
|
||||
eye_icon = QIcon.fromTheme("view-visible")
|
||||
if not eye_icon.isNull():
|
||||
self.api_show_btn.setIcon(eye_icon)
|
||||
self.api_show_btn.setText("")
|
||||
else:
|
||||
self.api_show_btn.setIcon(QIcon())
|
||||
self.api_show_btn.setText("\U0001F441")
|
||||
if checked:
|
||||
self.api_key_edit.setEchoMode(QLineEdit.Normal)
|
||||
self.api_show_btn.setStyleSheet("QToolButton { color: #4fc3f7; }")
|
||||
else:
|
||||
self.api_key_edit.setEchoMode(QLineEdit.Password)
|
||||
self.api_show_btn.setStyleSheet("")
|
||||
|
||||
def _pick_directory(self, line_edit):
|
||||
dir_path = QFileDialog.getExistingDirectory(self, "Select Directory", line_edit.text() or os.path.expanduser("~"))
|
||||
if dir_path:
|
||||
line_edit.setText(dir_path)
|
||||
|
||||
def _show_help(self):
|
||||
MessageService.information(self, "Help", "Help/documentation coming soon!", safety_level="low")
|
||||
|
||||
def _load_json(self, path):
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def _save_json(self, path, data):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
except Exception as e:
|
||||
MessageService.warning(self, "Error", f"Failed to save {path}: {e}", safety_level="medium")
|
||||
|
||||
def _clear_api_key(self):
|
||||
self.api_key_edit.setText("")
|
||||
self.config_handler.clear_api_key()
|
||||
MessageService.information(self, "API Key Cleared", "Nexus API Key has been cleared.", safety_level="low")
|
||||
|
||||
def _on_api_key_changed(self, text):
|
||||
api_key = text.strip()
|
||||
self.config_handler.save_api_key(api_key)
|
||||
|
||||
def _update_oauth_status(self):
|
||||
from jackify.backend.services.nexus_auth_service import NexusAuthService
|
||||
auth_service = NexusAuthService()
|
||||
authenticated, method, username = auth_service.get_auth_status()
|
||||
if authenticated and method == 'oauth':
|
||||
self.oauth_status_label.setText(f"Authorised as {username}" if username else "Authorised")
|
||||
self.oauth_status_label.setStyleSheet("color: #3fd0ea;")
|
||||
self.oauth_btn.setText("Revoke")
|
||||
elif method == 'oauth_expired':
|
||||
self.oauth_status_label.setText("OAuth token expired")
|
||||
self.oauth_status_label.setStyleSheet("color: #FFA726;")
|
||||
self.oauth_btn.setText("Re-authorise")
|
||||
else:
|
||||
self.oauth_status_label.setText("Not authorised")
|
||||
self.oauth_status_label.setStyleSheet("color: #f44336;")
|
||||
self.oauth_btn.setText("Authorise")
|
||||
|
||||
def _handle_oauth_click(self):
|
||||
from jackify.backend.services.nexus_auth_service import NexusAuthService
|
||||
auth_service = NexusAuthService()
|
||||
authenticated, method, _ = auth_service.get_auth_status()
|
||||
if authenticated and method == 'oauth':
|
||||
reply = MessageService.question(self, "Revoke", "Revoke OAuth authorisation?", safety_level="low")
|
||||
if reply == QMessageBox.Yes:
|
||||
auth_service.revoke_oauth()
|
||||
self._update_oauth_status()
|
||||
MessageService.information(self, "Revoked", "OAuth authorisation has been revoked.", safety_level="low")
|
||||
else:
|
||||
reply = MessageService.question(self, "Authorise with Nexus",
|
||||
"Your browser will open for Nexus authorisation.\n\n"
|
||||
"Note: Your browser may ask permission to open 'xdg-open'\n"
|
||||
"or Jackify's protocol handler - please click 'Open' or 'Allow'.\n\n"
|
||||
"Please log in and authorise Jackify when prompted.\n\n"
|
||||
"Continue?", safety_level="low")
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
progress = QProgressDialog(
|
||||
"Waiting for authorisation...\n\nPlease check your browser.",
|
||||
"Cancel", 0, 0, self
|
||||
)
|
||||
progress.setWindowTitle("Nexus OAuth")
|
||||
progress.setWindowModality(Qt.WindowModal)
|
||||
progress.setMinimumDuration(0)
|
||||
progress.setMinimumWidth(400)
|
||||
progress.show()
|
||||
QApplication.processEvents()
|
||||
def show_message(msg):
|
||||
progress.setLabelText(f"Waiting for authorisation...\n\n{msg}")
|
||||
QApplication.processEvents()
|
||||
success = auth_service.authorize_oauth(show_browser_message_callback=show_message)
|
||||
progress.close()
|
||||
QApplication.processEvents()
|
||||
self._update_oauth_status()
|
||||
if success:
|
||||
_, _, username = auth_service.get_auth_status()
|
||||
msg = "OAuth authorisation successful!"
|
||||
if username:
|
||||
msg += f"\n\nAuthorised as: {username}"
|
||||
MessageService.information(self, "Success", msg, safety_level="low")
|
||||
else:
|
||||
MessageService.warning(self, "Failed", "OAuth authorisation failed or was cancelled.", safety_level="low")
|
||||
|
||||
def _save(self):
|
||||
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
|
||||
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
|
||||
# Convert KB/s to bytes/s for storage (resource_settings.json expects bytes)
|
||||
bandwidth_kb = self.bandwidth_spin.value()
|
||||
bandwidth_bytes = bandwidth_kb * 1024
|
||||
self.resource_settings["Downloads"]["MaxThroughput"] = bandwidth_bytes
|
||||
|
||||
# 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())
|
||||
# OAuth disabled for v0.1.8 - no fallback setting needed
|
||||
# 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)
|
||||
|
||||
# Initialize with existing config values as fallback (prevents UnboundLocalError if auto-detection fails)
|
||||
resolved_install_path = self.config_handler.get("proton_path", "")
|
||||
resolved_install_version = self.config_handler.get("proton_version", "")
|
||||
|
||||
# Save Install Proton selection - resolve "auto" to actual path
|
||||
selected_install_proton_path = self.install_proton_dropdown.currentData()
|
||||
if selected_install_proton_path == "none":
|
||||
# No Proton detected - warn user but allow saving other settings
|
||||
MessageService.warning(
|
||||
self,
|
||||
"No Compatible Proton Installed",
|
||||
"Jackify requires Proton 9.0+, Proton Experimental, or GE-Proton 10+ to install modlists.\n\n"
|
||||
"To install Proton:\n"
|
||||
"1. Install any Windows game in Steam (Proton downloads automatically), OR\n"
|
||||
"2. Install GE-Proton using ProtonPlus or ProtonUp-Qt, OR\n"
|
||||
"3. Download GE-Proton manually from:\n"
|
||||
" https://github.com/GloriousEggroll/proton-ge-custom/releases\n\n"
|
||||
"Your other settings will be saved, but modlist installation may not work without Proton.",
|
||||
safety_level="medium"
|
||||
)
|
||||
logger.warning("No Proton detected - user warned, allowing save to proceed for other settings")
|
||||
# Don't modify Proton config, but continue to save other settings
|
||||
elif 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']
|
||||
self.config_handler.set("proton_path", resolved_install_path)
|
||||
self.config_handler.set("proton_version", resolved_install_version)
|
||||
else:
|
||||
# No Proton found - don't write anything, let engine auto-detect
|
||||
logger.warning("Auto Proton selection failed: No Proton versions found")
|
||||
# Don't modify existing config values
|
||||
except Exception as e:
|
||||
# Exception during detection - log it and don't write anything
|
||||
logger.error(f"Auto Proton selection failed with exception: {e}", exc_info=True)
|
||||
# Don't modify existing config values
|
||||
else:
|
||||
# User selected specific Proton version
|
||||
resolved_install_path = selected_install_proton_path
|
||||
resolved_install_version = self.install_proton_dropdown.currentText()
|
||||
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
|
||||
if self.winetricks_radio.isChecked():
|
||||
method = 'winetricks'
|
||||
else: # protontricks_radio (alternative)
|
||||
method = 'system_protontricks'
|
||||
|
||||
old_method = self.config_handler.get('component_installation_method', 'winetricks')
|
||||
method_changed = (old_method != method)
|
||||
|
||||
self.config_handler.set("component_installation_method", method)
|
||||
self.config_handler.set("use_winetricks_for_components", method == 'winetricks')
|
||||
|
||||
# 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:
|
||||
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
|
||||
# Check protontricks if user just switched to it
|
||||
if method_changed and method == 'system_protontricks':
|
||||
main_window = self.parent()
|
||||
if main_window and hasattr(main_window, 'protontricks_service'):
|
||||
is_installed, installation_type, details = main_window.protontricks_service.detect_protontricks(use_cache=False)
|
||||
if not is_installed:
|
||||
from jackify.frontends.gui.dialogs.protontricks_error_dialog import ProtontricksErrorDialog
|
||||
dialog = ProtontricksErrorDialog(main_window.protontricks_service, main_window)
|
||||
dialog.exec()
|
||||
|
||||
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."""
|
||||
try:
|
||||
# Get the main window through parent relationship
|
||||
main_window = self.parent()
|
||||
if not main_window or not hasattr(main_window, 'stacked_widget'):
|
||||
return
|
||||
|
||||
# Refresh paths in all screens that have the method
|
||||
screens_to_refresh = [
|
||||
getattr(main_window, 'install_modlist_screen', None),
|
||||
getattr(main_window, 'configure_new_modlist_screen', None),
|
||||
getattr(main_window, 'configure_existing_modlist_screen', None),
|
||||
]
|
||||
|
||||
for screen in screens_to_refresh:
|
||||
if screen and hasattr(screen, 'refresh_paths'):
|
||||
screen.refresh_paths()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not refresh GUI paths: {e}")
|
||||
|
||||
def _bold_label(self, text):
|
||||
label = QLabel(text)
|
||||
label.setStyleSheet("font-weight: bold; color: #fff;")
|
||||
return label
|
||||
|
||||
|
||||
114
jackify/frontends/gui/dialogs/settings_dialog_proton.py
Normal file
114
jackify/frontends/gui/dialogs/settings_dialog_proton.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Settings dialog Proton dropdown population and refresh.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsDialogProtonMixin:
|
||||
"""Mixin providing Proton dropdown population and refresh for SettingsDialog."""
|
||||
|
||||
def _get_proton_10_path(self):
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
available_protons = WineUtils.scan_valve_proton_versions()
|
||||
for proton in available_protons:
|
||||
if proton['version'].startswith('10.'):
|
||||
return proton['path']
|
||||
return 'auto'
|
||||
except Exception:
|
||||
return 'auto'
|
||||
|
||||
def _populate_install_proton_dropdown(self):
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
available_protons = WineUtils.scan_all_proton_versions()
|
||||
has_proton = len(available_protons) > 0
|
||||
if has_proton:
|
||||
self.install_proton_dropdown.addItem("Auto (Recommended)", "auto")
|
||||
else:
|
||||
self.install_proton_dropdown.addItem("No Proton Versions Detected", "none")
|
||||
|
||||
fast_protons = []
|
||||
slow_protons = []
|
||||
for proton in available_protons:
|
||||
proton_name = proton.get('name', 'Unknown Proton')
|
||||
proton_type = proton.get('type', 'Unknown')
|
||||
if proton_type not in ('GE-Proton', 'Valve-Proton'):
|
||||
logger.debug(
|
||||
"Skipping %s (%s) from Install Proton dropdown - third-party builds may have compatibility issues",
|
||||
proton_name, proton_type
|
||||
)
|
||||
continue
|
||||
slow_warning = False
|
||||
is_fast_proton = False
|
||||
display_name = proton_name
|
||||
if proton_name == "Proton - Experimental":
|
||||
is_fast_proton = True
|
||||
elif proton_type == 'GE-Proton':
|
||||
major_version = proton.get('major_version')
|
||||
if major_version is not None and isinstance(major_version, int) and major_version >= 10:
|
||||
is_fast_proton = True
|
||||
elif 'GE-Proton9' in proton_name or 'GE-Proton8' in proton_name:
|
||||
slow_warning = True
|
||||
display_name = f"{proton_name} (GE)"
|
||||
elif proton_type == 'Valve-Proton':
|
||||
if proton_name.startswith("Proton 9") or "9.0" in proton_name:
|
||||
slow_warning = True
|
||||
if slow_warning:
|
||||
display_name = f"{display_name} (Slow texture processing)"
|
||||
slow_protons.append((display_name, str(proton['path'])))
|
||||
else:
|
||||
fast_protons.append((display_name, str(proton['path'])))
|
||||
|
||||
for display_name, path in fast_protons:
|
||||
self.install_proton_dropdown.addItem(display_name, path)
|
||||
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)
|
||||
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("Failed to populate install Proton dropdown: %s", e)
|
||||
self.install_proton_dropdown.addItem("Auto (Recommended)", "auto")
|
||||
|
||||
def _populate_game_proton_dropdown(self):
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
available_protons = WineUtils.scan_all_proton_versions()
|
||||
self.game_proton_dropdown.addItem("Same as Install Proton", "same_as_install")
|
||||
for proton in available_protons:
|
||||
proton_name = proton.get('name', 'Unknown Proton')
|
||||
proton_type = proton.get('type', 'Unknown')
|
||||
display_name = f"{proton_name} (GE)" if proton_type == 'GE-Proton' else proton_name
|
||||
self.game_proton_dropdown.addItem(display_name, str(proton['path']))
|
||||
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("Failed to populate game Proton dropdown: %s", e)
|
||||
self.game_proton_dropdown.addItem("Same as Install Proton", "same_as_install")
|
||||
|
||||
def _set_dropdown_selection(self, dropdown, 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 not found_match and saved_value not in ["auto", "same_as_install"]:
|
||||
dropdown.setCurrentIndex(0)
|
||||
|
||||
def _refresh_install_proton_dropdown(self):
|
||||
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):
|
||||
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)
|
||||
280
jackify/frontends/gui/dialogs/settings_dialog_tabs.py
Normal file
280
jackify/frontends/gui/dialogs/settings_dialog_tabs.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Settings dialog tab creation: General and Advanced tabs.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QCheckBox,
|
||||
QComboBox, QGroupBox, QFormLayout, QGridLayout, QSpinBox, QRadioButton, QButtonGroup,
|
||||
QToolButton
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsDialogTabsMixin:
|
||||
"""Mixin providing _create_general_tab and _create_advanced_tab for SettingsDialog."""
|
||||
|
||||
def _create_general_tab(self):
|
||||
general_tab = QWidget()
|
||||
general_layout = QVBoxLayout(general_tab)
|
||||
|
||||
dir_group = QGroupBox("Directory Paths")
|
||||
dir_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; }")
|
||||
dir_layout = QFormLayout()
|
||||
dir_group.setLayout(dir_layout)
|
||||
self.install_dir_edit = QLineEdit(self.config_handler.get("modlist_install_base_dir", ""))
|
||||
self.install_dir_edit.setToolTip("Default directory for modlist installations.")
|
||||
self.install_dir_btn = QPushButton()
|
||||
self.install_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
|
||||
self.install_dir_btn.setToolTip("Browse for directory")
|
||||
self.install_dir_btn.setFixedWidth(32)
|
||||
self.install_dir_btn.clicked.connect(lambda: self._pick_directory(self.install_dir_edit))
|
||||
install_dir_row = QHBoxLayout()
|
||||
install_dir_row.addWidget(self.install_dir_edit)
|
||||
install_dir_row.addWidget(self.install_dir_btn)
|
||||
dir_layout.addRow(QLabel("Install Base Dir:"), install_dir_row)
|
||||
self.download_dir_edit = QLineEdit(self.config_handler.get("modlist_downloads_base_dir", ""))
|
||||
self.download_dir_edit.setToolTip("Default directory for modlist downloads.")
|
||||
self.download_dir_btn = QPushButton()
|
||||
self.download_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
|
||||
self.download_dir_btn.setToolTip("Browse for directory")
|
||||
self.download_dir_btn.setFixedWidth(32)
|
||||
self.download_dir_btn.clicked.connect(lambda: self._pick_directory(self.download_dir_edit))
|
||||
download_dir_row = QHBoxLayout()
|
||||
download_dir_row.addWidget(self.download_dir_edit)
|
||||
download_dir_row.addWidget(self.download_dir_btn)
|
||||
dir_layout.addRow(QLabel("Downloads Base Dir:"), download_dir_row)
|
||||
|
||||
from jackify.shared.paths import get_jackify_data_dir
|
||||
current_jackify_dir = str(get_jackify_data_dir())
|
||||
self.jackify_data_dir_edit = QLineEdit(current_jackify_dir)
|
||||
self.jackify_data_dir_edit.setToolTip("Directory for Jackify data (logs, downloads, temp files). Default: ~/Jackify")
|
||||
self.jackify_data_dir_btn = QPushButton()
|
||||
self.jackify_data_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
|
||||
self.jackify_data_dir_btn.setToolTip("Browse for directory")
|
||||
self.jackify_data_dir_btn.setFixedWidth(32)
|
||||
self.jackify_data_dir_btn.clicked.connect(lambda: self._pick_directory(self.jackify_data_dir_edit))
|
||||
jackify_data_dir_row = QHBoxLayout()
|
||||
jackify_data_dir_row.addWidget(self.jackify_data_dir_edit)
|
||||
jackify_data_dir_row.addWidget(self.jackify_data_dir_btn)
|
||||
reset_jackify_dir_btn = QPushButton("Reset")
|
||||
reset_jackify_dir_btn.setToolTip("Reset to default (~/ Jackify)")
|
||||
reset_jackify_dir_btn.setFixedWidth(50)
|
||||
reset_jackify_dir_btn.clicked.connect(lambda: self.jackify_data_dir_edit.setText(str(Path.home() / "Jackify")))
|
||||
jackify_data_dir_row.addWidget(reset_jackify_dir_btn)
|
||||
dir_layout.addRow(QLabel("Jackify Data Dir:"), jackify_data_dir_row)
|
||||
general_layout.addWidget(dir_group)
|
||||
general_layout.addSpacing(12)
|
||||
|
||||
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 = QVBoxLayout()
|
||||
proton_group.setLayout(proton_layout)
|
||||
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)
|
||||
install_refresh_btn = QPushButton("\u21BB")
|
||||
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)
|
||||
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()
|
||||
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("\u21BB")
|
||||
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)
|
||||
self._populate_install_proton_dropdown()
|
||||
self._populate_game_proton_dropdown()
|
||||
general_layout.addWidget(proton_group)
|
||||
general_layout.addSpacing(12)
|
||||
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
oauth_group = QGroupBox("Nexus Authentication")
|
||||
oauth_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; }")
|
||||
oauth_layout = QVBoxLayout()
|
||||
oauth_group.setLayout(oauth_layout)
|
||||
oauth_status_layout = QHBoxLayout()
|
||||
self.oauth_status_label = QLabel("Checking...")
|
||||
self.oauth_status_label.setStyleSheet("color: #ccc;")
|
||||
self.oauth_btn = QPushButton("Authorise")
|
||||
self.oauth_btn.setMaximumWidth(100)
|
||||
self.oauth_btn.clicked.connect(self._handle_oauth_click)
|
||||
oauth_status_layout.addWidget(QLabel("Status:"))
|
||||
oauth_status_layout.addWidget(self.oauth_status_label)
|
||||
oauth_status_layout.addWidget(self.oauth_btn)
|
||||
oauth_status_layout.addStretch()
|
||||
oauth_layout.addLayout(oauth_status_layout)
|
||||
self._update_oauth_status()
|
||||
general_layout.addWidget(oauth_group)
|
||||
general_layout.addSpacing(12)
|
||||
|
||||
debug_group = QGroupBox("Enable Debug")
|
||||
debug_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; }")
|
||||
debug_layout = QVBoxLayout()
|
||||
debug_group.setLayout(debug_layout)
|
||||
self.debug_checkbox = QCheckBox("Enable debug mode (requires restart)")
|
||||
self.debug_checkbox.setChecked(self.config_handler.get('debug_mode', False))
|
||||
self.debug_checkbox.setToolTip("Enable verbose debug logging. Requires Jackify restart to take effect.")
|
||||
self.debug_checkbox.setStyleSheet("color: #fff;")
|
||||
debug_layout.addWidget(self.debug_checkbox)
|
||||
general_layout.addWidget(debug_group)
|
||||
general_layout.addStretch()
|
||||
self.tab_widget.addTab(general_tab, "General")
|
||||
|
||||
def _create_advanced_tab(self):
|
||||
advanced_tab = QWidget()
|
||||
advanced_layout = QVBoxLayout(advanced_tab)
|
||||
auth_group = QGroupBox("Nexus Authentication")
|
||||
auth_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; }")
|
||||
auth_layout = QVBoxLayout()
|
||||
auth_group.setLayout(auth_layout)
|
||||
api_layout = QHBoxLayout()
|
||||
self.api_key_edit = QLineEdit()
|
||||
self.api_key_edit.setEchoMode(QLineEdit.Password)
|
||||
api_key = self.config_handler.get_api_key()
|
||||
self.api_key_edit.setText(api_key if api_key else "")
|
||||
self.api_key_edit.setToolTip("Your Nexus API Key (legacy authentication method)")
|
||||
self.api_key_edit.textChanged.connect(self._on_api_key_changed)
|
||||
self.api_show_btn = QToolButton()
|
||||
self.api_show_btn.setCheckable(True)
|
||||
self.api_show_btn.setIcon(QIcon.fromTheme("view-visible"))
|
||||
self.api_show_btn.setToolTip("Show or hide your API key")
|
||||
self.api_show_btn.toggled.connect(self._toggle_api_key_visibility)
|
||||
clear_api_btn = QPushButton("Clear")
|
||||
clear_api_btn.clicked.connect(self._clear_api_key)
|
||||
clear_api_btn.setMaximumWidth(60)
|
||||
api_layout.addWidget(QLabel("API Key:"))
|
||||
api_layout.addWidget(self.api_key_edit)
|
||||
api_layout.addWidget(self.api_show_btn)
|
||||
api_layout.addWidget(clear_api_btn)
|
||||
auth_layout.addLayout(api_layout)
|
||||
advanced_layout.addWidget(auth_group)
|
||||
advanced_layout.addSpacing(12)
|
||||
|
||||
self.resource_settings_path = os.path.expanduser("~/.config/jackify/resource_settings.json")
|
||||
self.resource_settings = self._load_json(self.resource_settings_path)
|
||||
self.resource_edits = {}
|
||||
resource_group = QGroupBox("Resource Limits")
|
||||
resource_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; }")
|
||||
resource_outer_layout = QVBoxLayout()
|
||||
resource_group.setLayout(resource_outer_layout)
|
||||
if not self.resource_settings:
|
||||
info_label = QLabel("Resource Limit settings will be generated once a modlist install action is performed")
|
||||
info_label.setStyleSheet("color: #aaa; font-style: italic; padding: 20px; font-size: 11pt;")
|
||||
info_label.setWordWrap(True)
|
||||
info_label.setAlignment(Qt.AlignCenter)
|
||||
info_label.setMinimumHeight(60)
|
||||
resource_outer_layout.addWidget(info_label)
|
||||
else:
|
||||
resource_grid = QGridLayout()
|
||||
resource_grid.setVerticalSpacing(4)
|
||||
resource_grid.setHorizontalSpacing(8)
|
||||
resource_grid.setColumnMinimumWidth(2, 40)
|
||||
resource_grid.addWidget(self._bold_label("Resource"), 0, 0, 1, 1, Qt.AlignLeft)
|
||||
resource_grid.addWidget(self._bold_label("Max Tasks"), 0, 1, 1, 1, Qt.AlignLeft)
|
||||
resource_grid.addWidget(self._bold_label("Resource"), 0, 3, 1, 1, Qt.AlignLeft)
|
||||
resource_grid.addWidget(self._bold_label("Max Tasks"), 0, 4, 1, 1, Qt.AlignLeft)
|
||||
resource_items = list(self.resource_settings.items())
|
||||
bandwidth_kb = 0
|
||||
if "Downloads" in self.resource_settings:
|
||||
bandwidth_kb = self.resource_settings["Downloads"].get("MaxThroughput", 0) // 1024 or 0
|
||||
left_row = 1
|
||||
for k, v in resource_items[:4]:
|
||||
try:
|
||||
resource_grid.addWidget(QLabel(f"{k}:", parent=self), left_row, 0, 1, 1, Qt.AlignLeft)
|
||||
max_tasks_spin = QSpinBox()
|
||||
max_tasks_spin.setMinimum(1)
|
||||
max_tasks_spin.setMaximum(128)
|
||||
max_tasks_spin.setValue(v.get('MaxTasks', 16))
|
||||
max_tasks_spin.setToolTip("Maximum number of concurrent tasks for this resource.")
|
||||
max_tasks_spin.setFixedWidth(100)
|
||||
resource_grid.addWidget(max_tasks_spin, left_row, 1)
|
||||
self.resource_edits[k] = (None, max_tasks_spin)
|
||||
left_row += 1
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to create widgets for resource '%s': %s", k, e)
|
||||
continue
|
||||
right_row = 1
|
||||
for k, v in resource_items[4:]:
|
||||
try:
|
||||
resource_grid.addWidget(QLabel(f"{k}:", parent=self), right_row, 3, 1, 1, Qt.AlignLeft)
|
||||
max_tasks_spin = QSpinBox()
|
||||
max_tasks_spin.setMinimum(1)
|
||||
max_tasks_spin.setMaximum(128)
|
||||
max_tasks_spin.setValue(v.get('MaxTasks', 16))
|
||||
max_tasks_spin.setToolTip("Maximum number of concurrent tasks for this resource.")
|
||||
max_tasks_spin.setFixedWidth(100)
|
||||
resource_grid.addWidget(max_tasks_spin, right_row, 4)
|
||||
self.resource_edits[k] = (None, max_tasks_spin)
|
||||
right_row += 1
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to create widgets for resource '%s': %s", k, e)
|
||||
continue
|
||||
if "Downloads" in self.resource_settings:
|
||||
resource_grid.addWidget(QLabel("Bandwidth Limit:", parent=self), right_row, 3, 1, 1, Qt.AlignLeft)
|
||||
self.bandwidth_spin = QSpinBox()
|
||||
self.bandwidth_spin.setMinimum(0)
|
||||
self.bandwidth_spin.setMaximum(1000000)
|
||||
self.bandwidth_spin.setValue(bandwidth_kb)
|
||||
self.bandwidth_spin.setSuffix(" KB/s")
|
||||
self.bandwidth_spin.setFixedWidth(100)
|
||||
self.bandwidth_spin.setToolTip("Set the maximum download speed for modlist downloads. 0 = unlimited.")
|
||||
bandwidth_widget_layout = QHBoxLayout()
|
||||
bandwidth_widget_layout.setContentsMargins(0, 0, 0, 0)
|
||||
bandwidth_widget_layout.addWidget(self.bandwidth_spin)
|
||||
bandwidth_note = QLabel("(0 = unlimited)")
|
||||
bandwidth_note.setStyleSheet("color: #aaa; font-size: 9pt;")
|
||||
bandwidth_widget_layout.addWidget(bandwidth_note)
|
||||
bandwidth_widget_layout.addStretch()
|
||||
bandwidth_container = QWidget()
|
||||
bandwidth_container.setLayout(bandwidth_widget_layout)
|
||||
resource_grid.addWidget(bandwidth_container, right_row, 4, 1, 1, Qt.AlignLeft)
|
||||
else:
|
||||
self.bandwidth_spin = None
|
||||
resource_grid.setColumnStretch(5, 1)
|
||||
resource_outer_layout.addLayout(resource_grid)
|
||||
advanced_layout.addWidget(resource_group)
|
||||
|
||||
component_group = QGroupBox("Advanced Tool Options")
|
||||
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)
|
||||
component_layout.addWidget(QLabel("Wine Components Installation:"))
|
||||
self.component_method_group = QButtonGroup()
|
||||
component_method_layout = QVBoxLayout()
|
||||
current_method = self.config_handler.get('component_installation_method', 'winetricks')
|
||||
if current_method == 'bundled_protontricks':
|
||||
current_method = 'system_protontricks'
|
||||
self.winetricks_radio = QRadioButton("Winetricks (Default)")
|
||||
self.winetricks_radio.setChecked(current_method == 'winetricks')
|
||||
self.winetricks_radio.setToolTip("Use bundled winetricks for component installation. Faster and more reliable.")
|
||||
self.component_method_group.addButton(self.winetricks_radio, 0)
|
||||
component_method_layout.addWidget(self.winetricks_radio)
|
||||
self.protontricks_radio = QRadioButton("Protontricks (Alternative)")
|
||||
self.protontricks_radio.setChecked(current_method == 'system_protontricks')
|
||||
self.protontricks_radio.setToolTip("Use system-installed protontricks (flatpak or native). Fallback option if winetricks fails.")
|
||||
self.component_method_group.addButton(self.protontricks_radio, 1)
|
||||
component_method_layout.addWidget(self.protontricks_radio)
|
||||
component_layout.addLayout(component_method_layout)
|
||||
advanced_layout.addWidget(component_group)
|
||||
advanced_layout.addStretch()
|
||||
self.tab_widget.addTab(advanced_tab, "Advanced")
|
||||
@@ -244,7 +244,7 @@ class SuccessDialog(QDialog):
|
||||
else:
|
||||
base_message = f"You can now launch {self.modlist_name} from Steam and enjoy your modded {game_display} experience!"
|
||||
|
||||
# Note: ENB-specific Proton warning is now shown in a separate dialog when ENB is detected
|
||||
# ENB Proton warning shown in separate dialog
|
||||
return base_message
|
||||
|
||||
def _update_countdown(self):
|
||||
|
||||
Reference in New Issue
Block a user