mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 03:37:44 +02:00
Initial public release v0.1.0 - Linux Wabbajack Modlist Application
Jackify provides native Linux support for Wabbajack modlist installation and management with automated Steam integration and Proton configuration. Key Features: - Almost Native Linux implementation (texconv.exe run via proton) - Automated Steam shortcut creation and Proton prefix management - Both CLI and GUI interfaces, with Steam Deck optimization Supported Games: - Skyrim Special Edition - Fallout 4 - Fallout New Vegas - Oblivion, Starfield, Enderal, and diverse other games Technical Architecture: - Clean separation between frontend and backend services - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
This commit is contained in:
10
jackify/frontends/gui/dialogs/__init__.py
Normal file
10
jackify/frontends/gui/dialogs/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
GUI Dialogs Package
|
||||
|
||||
Custom dialogs for the Jackify GUI application.
|
||||
"""
|
||||
|
||||
from .completion_dialog import NextStepsDialog
|
||||
from .success_dialog import SuccessDialog
|
||||
|
||||
__all__ = ['NextStepsDialog', 'SuccessDialog']
|
||||
200
jackify/frontends/gui/dialogs/completion_dialog.py
Normal file
200
jackify/frontends/gui/dialogs/completion_dialog.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Completion Dialog
|
||||
|
||||
Custom completion dialog that shows the same detailed completion message
|
||||
as the CLI frontend, formatted for GUI display.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit,
|
||||
QWidget, QSpacerItem, QSizePolicy
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QPixmap, QIcon
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NextStepsDialog(QDialog):
|
||||
"""
|
||||
Custom completion dialog showing detailed next steps after modlist configuration.
|
||||
|
||||
Displays the same information as the CLI completion message but in a proper GUI format.
|
||||
"""
|
||||
|
||||
def __init__(self, modlist_name: str, parent=None):
|
||||
"""
|
||||
Initialize the Next Steps dialog.
|
||||
|
||||
Args:
|
||||
modlist_name: Name of the configured modlist
|
||||
parent: Parent widget
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.modlist_name = modlist_name
|
||||
self.setWindowTitle("Next Steps")
|
||||
self.setModal(True)
|
||||
self.setFixedSize(600, 400)
|
||||
|
||||
# Set the Wabbajack icon if available
|
||||
self._set_dialog_icon()
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
logger.info(f"NextStepsDialog created for modlist: {modlist_name}")
|
||||
|
||||
def _set_dialog_icon(self):
|
||||
"""Set the dialog icon to Wabbajack icon if available"""
|
||||
try:
|
||||
# Try to use the same icon as the main application
|
||||
icon_path = Path(__file__).parent.parent.parent.parent.parent / "Files" / "wabbajack-icon.png"
|
||||
if icon_path.exists():
|
||||
icon = QIcon(str(icon_path))
|
||||
self.setWindowIcon(icon)
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not set dialog icon: {e}")
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the dialog user interface"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
# Header with icon and title
|
||||
self._setup_header(layout)
|
||||
|
||||
# Main content area
|
||||
self._setup_content(layout)
|
||||
|
||||
# Action buttons
|
||||
self._setup_buttons(layout)
|
||||
|
||||
def _setup_header(self, layout):
|
||||
"""Set up the dialog header with title"""
|
||||
header_layout = QHBoxLayout()
|
||||
|
||||
# Title
|
||||
title_label = QLabel("Next Steps:")
|
||||
title_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 18px; "
|
||||
" font-weight: bold; "
|
||||
" color: #2c3e50; "
|
||||
" margin-bottom: 10px; "
|
||||
"}"
|
||||
)
|
||||
header_layout.addWidget(title_label)
|
||||
|
||||
# Add some space
|
||||
header_layout.addStretch()
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
def _setup_content(self, layout):
|
||||
"""Set up the main content area with next steps"""
|
||||
# Create content area
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
content_layout.setSpacing(12)
|
||||
|
||||
# Add the detailed next steps text (matching CLI completion message)
|
||||
steps_text = self._build_completion_text()
|
||||
|
||||
content_text = QTextEdit()
|
||||
content_text.setPlainText(steps_text)
|
||||
content_text.setReadOnly(True)
|
||||
content_text.setStyleSheet(
|
||||
"QTextEdit { "
|
||||
" background-color: #f8f9fa; "
|
||||
" border: 1px solid #dee2e6; "
|
||||
" border-radius: 6px; "
|
||||
" padding: 12px; "
|
||||
" font-family: 'Segoe UI', Arial, sans-serif; "
|
||||
" font-size: 12px; "
|
||||
" line-height: 1.5; "
|
||||
"}"
|
||||
)
|
||||
content_layout.addWidget(content_text)
|
||||
|
||||
layout.addWidget(content_widget)
|
||||
|
||||
def _setup_buttons(self, layout):
|
||||
"""Set up the action buttons"""
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(12)
|
||||
|
||||
# Add stretch to center buttons
|
||||
button_layout.addStretch()
|
||||
|
||||
# Return button (goes back to menu)
|
||||
return_btn = QPushButton("Return")
|
||||
return_btn.setFixedSize(100, 35)
|
||||
return_btn.clicked.connect(self.accept) # This will close dialog and return to menu
|
||||
return_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #3498db; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
" font-weight: bold; "
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #2980b9; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #21618c; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(return_btn)
|
||||
|
||||
button_layout.addSpacing(10)
|
||||
|
||||
# Exit button (closes the application)
|
||||
exit_btn = QPushButton("Exit")
|
||||
exit_btn.setFixedSize(100, 35)
|
||||
exit_btn.clicked.connect(self.reject) # This will close dialog and potentially exit app
|
||||
exit_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #95a5a6; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
" font-weight: bold; "
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #7f8c8d; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #6c7b7d; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(exit_btn)
|
||||
|
||||
button_layout.addStretch()
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def _build_completion_text(self) -> str:
|
||||
"""
|
||||
Build the completion text matching the CLI version from menu_handler.py.
|
||||
|
||||
Returns:
|
||||
Formatted completion text string
|
||||
"""
|
||||
# Match the CLI completion text from menu_handler.py lines 627-631
|
||||
completion_text = f"""✓ Configuration completed successfully!
|
||||
|
||||
Modlist Install and Configuration complete!:
|
||||
|
||||
• You should now be able to Launch '{self.modlist_name}' through Steam.
|
||||
• Congratulations and enjoy the game!
|
||||
|
||||
Detailed log available at: ~/Jackify/logs/Configure_New_Modlist_workflow.log"""
|
||||
|
||||
return completion_text
|
||||
328
jackify/frontends/gui/dialogs/protontricks_error_dialog.py
Normal file
328
jackify/frontends/gui/dialogs/protontricks_error_dialog.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""
|
||||
Protontricks Error Dialog
|
||||
|
||||
Dialog shown when protontricks is not found, with options to install via Flatpak or get native installation guidance.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QSizePolicy, QTextEdit, QProgressBar
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QPixmap, QIcon, QFont
|
||||
from .. import shared_theme
|
||||
|
||||
|
||||
class FlatpakInstallThread(QThread):
|
||||
"""Thread for installing Flatpak protontricks"""
|
||||
finished = Signal(bool, str) # success, message
|
||||
|
||||
def __init__(self, detection_service):
|
||||
super().__init__()
|
||||
self.detection_service = detection_service
|
||||
|
||||
def run(self):
|
||||
success, message = self.detection_service.install_flatpak_protontricks()
|
||||
self.finished.emit(success, message)
|
||||
|
||||
|
||||
class ProtontricksErrorDialog(QDialog):
|
||||
"""
|
||||
Dialog shown when protontricks is not found
|
||||
Provides options to install via Flatpak or get native installation guidance
|
||||
"""
|
||||
|
||||
def __init__(self, detection_service, parent=None):
|
||||
super().__init__(parent)
|
||||
self.detection_service = detection_service
|
||||
self.setWindowTitle("Protontricks Required")
|
||||
self.setModal(True)
|
||||
self.setFixedSize(550, 520)
|
||||
self.install_thread = None
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Card background
|
||||
card = QFrame(self)
|
||||
card.setObjectName("protontricksCard")
|
||||
card.setFrameShape(QFrame.StyledPanel)
|
||||
card.setFrameShadow(QFrame.Raised)
|
||||
card.setMinimumWidth(500)
|
||||
card.setMinimumHeight(400)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
card_layout = QVBoxLayout(card)
|
||||
card_layout.setSpacing(16)
|
||||
card_layout.setContentsMargins(28, 28, 28, 28)
|
||||
card.setStyleSheet(
|
||||
"QFrame#protontricksCard { "
|
||||
" background: #2d2323; "
|
||||
" border-radius: 12px; "
|
||||
" border: 2px solid #e74c3c; "
|
||||
"}"
|
||||
)
|
||||
|
||||
# Error icon
|
||||
icon_label = QLabel()
|
||||
icon_label.setAlignment(Qt.AlignCenter)
|
||||
icon_label.setText("!")
|
||||
icon_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 36px; "
|
||||
" font-weight: bold; "
|
||||
" color: #e74c3c; "
|
||||
" margin-bottom: 4px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(icon_label)
|
||||
|
||||
# Error title
|
||||
title_label = QLabel("Protontricks Not Found")
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 20px; "
|
||||
" font-weight: 600; "
|
||||
" color: #e74c3c; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(title_label)
|
||||
|
||||
# Error message
|
||||
message_text = QTextEdit()
|
||||
message_text.setReadOnly(True)
|
||||
message_text.setPlainText(
|
||||
"Protontricks is required for Jackify to function properly. "
|
||||
"It manages Wine prefixes for Steam games and is essential for modlist installation and configuration.\n\n"
|
||||
"Choose an installation method below:"
|
||||
)
|
||||
message_text.setMinimumHeight(100)
|
||||
message_text.setMaximumHeight(120)
|
||||
message_text.setStyleSheet(
|
||||
"QTextEdit { "
|
||||
" font-size: 15px; "
|
||||
" color: #e0e0e0; "
|
||||
" background: transparent; "
|
||||
" border: none; "
|
||||
" line-height: 1.3; "
|
||||
" margin-bottom: 6px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(message_text)
|
||||
|
||||
# Progress bar (initially hidden)
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
self.progress_bar.setStyleSheet(
|
||||
"QProgressBar { "
|
||||
" border: 1px solid #555; "
|
||||
" border-radius: 4px; "
|
||||
" background: #23272e; "
|
||||
" text-align: center; "
|
||||
"} "
|
||||
"QProgressBar::chunk { "
|
||||
" background-color: #4fc3f7; "
|
||||
" border-radius: 3px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(self.progress_bar)
|
||||
|
||||
# Status label (initially hidden)
|
||||
self.status_label = QLabel()
|
||||
self.status_label.setVisible(False)
|
||||
self.status_label.setAlignment(Qt.AlignCenter)
|
||||
self.status_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 14px; "
|
||||
" color: #4fc3f7; "
|
||||
" margin: 8px 0; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(self.status_label)
|
||||
|
||||
# Button layout
|
||||
button_layout = QVBoxLayout()
|
||||
button_layout.setSpacing(12)
|
||||
|
||||
# Flatpak install button
|
||||
self.flatpak_btn = QPushButton("Install via Flatpak (Recommended)")
|
||||
self.flatpak_btn.setFixedHeight(40)
|
||||
self.flatpak_btn.clicked.connect(self._install_flatpak)
|
||||
self.flatpak_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #4fc3f7; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 6px; "
|
||||
" font-weight: bold; "
|
||||
" font-size: 14px; "
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #3498db; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #2980b9; "
|
||||
"} "
|
||||
"QPushButton:disabled { "
|
||||
" background-color: #555; "
|
||||
" color: #888; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(self.flatpak_btn)
|
||||
|
||||
# Native install guidance button
|
||||
self.native_btn = QPushButton("Show Native Installation Instructions")
|
||||
self.native_btn.setFixedHeight(40)
|
||||
self.native_btn.clicked.connect(self._show_native_guidance)
|
||||
self.native_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #95a5a6; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 6px; "
|
||||
" font-weight: bold; "
|
||||
" font-size: 14px; "
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #7f8c8d; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #6c7b7d; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(self.native_btn)
|
||||
|
||||
card_layout.addLayout(button_layout)
|
||||
|
||||
# Bottom button layout
|
||||
bottom_layout = QHBoxLayout()
|
||||
bottom_layout.setSpacing(12)
|
||||
|
||||
# Re-detect button
|
||||
self.redetect_btn = QPushButton("Re-detect")
|
||||
self.redetect_btn.setFixedSize(120, 36)
|
||||
self.redetect_btn.clicked.connect(self._redetect)
|
||||
self.redetect_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #27ae60; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
" font-weight: bold; "
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #229954; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #1e8449; "
|
||||
"}"
|
||||
)
|
||||
bottom_layout.addWidget(self.redetect_btn)
|
||||
|
||||
bottom_layout.addStretch()
|
||||
|
||||
# Exit button
|
||||
exit_btn = QPushButton("Exit Jackify")
|
||||
exit_btn.setFixedSize(120, 36)
|
||||
exit_btn.clicked.connect(self._exit_app)
|
||||
exit_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #e74c3c; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
" font-weight: bold; "
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #c0392b; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #a93226; "
|
||||
"}"
|
||||
)
|
||||
bottom_layout.addWidget(exit_btn)
|
||||
|
||||
card_layout.addLayout(bottom_layout)
|
||||
|
||||
layout.addStretch()
|
||||
layout.addWidget(card, alignment=Qt.AlignCenter)
|
||||
layout.addStretch()
|
||||
|
||||
def _install_flatpak(self):
|
||||
"""Install protontricks via Flatpak"""
|
||||
# Disable buttons during installation
|
||||
self.flatpak_btn.setEnabled(False)
|
||||
self.native_btn.setEnabled(False)
|
||||
self.redetect_btn.setEnabled(False)
|
||||
|
||||
# Show progress
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate progress
|
||||
self.status_label.setVisible(True)
|
||||
self.status_label.setText("Installing Flatpak protontricks...")
|
||||
|
||||
# Start installation thread
|
||||
self.install_thread = FlatpakInstallThread(self.detection_service)
|
||||
self.install_thread.finished.connect(self._on_install_finished)
|
||||
self.install_thread.start()
|
||||
|
||||
def _on_install_finished(self, success, message):
|
||||
"""Handle installation completion"""
|
||||
# Hide progress
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
# Re-enable buttons
|
||||
self.flatpak_btn.setEnabled(True)
|
||||
self.native_btn.setEnabled(True)
|
||||
self.redetect_btn.setEnabled(True)
|
||||
|
||||
if success:
|
||||
self.status_label.setText("✓ Installation successful!")
|
||||
self.status_label.setStyleSheet("QLabel { color: #27ae60; font-size: 14px; margin: 8px 0; }")
|
||||
# Auto-redetect after successful installation
|
||||
self._redetect()
|
||||
else:
|
||||
self.status_label.setText(f"✗ Installation failed: {message}")
|
||||
self.status_label.setStyleSheet("QLabel { color: #e74c3c; font-size: 14px; margin: 8px 0; }")
|
||||
|
||||
def _show_native_guidance(self):
|
||||
"""Show native installation guidance"""
|
||||
from ..services.message_service import MessageService
|
||||
guidance = self.detection_service.get_installation_guidance()
|
||||
MessageService.information(self, "Native Installation", guidance, safety_level="low")
|
||||
|
||||
def _redetect(self):
|
||||
"""Re-detect protontricks"""
|
||||
self.detection_service.clear_cache()
|
||||
is_installed, installation_type, details = self.detection_service.detect_protontricks(use_cache=False)
|
||||
|
||||
if is_installed:
|
||||
self.status_label.setText("✓ Protontricks found!")
|
||||
self.status_label.setStyleSheet("QLabel { color: #27ae60; font-size: 14px; margin: 8px 0; }")
|
||||
self.status_label.setVisible(True)
|
||||
self.accept() # Close dialog successfully
|
||||
else:
|
||||
self.status_label.setText("✗ Protontricks still not found")
|
||||
self.status_label.setStyleSheet("QLabel { color: #e74c3c; font-size: 14px; margin: 8px 0; }")
|
||||
self.status_label.setVisible(True)
|
||||
|
||||
def _exit_app(self):
|
||||
"""Exit the application"""
|
||||
self.reject()
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle dialog close event"""
|
||||
if self.install_thread and self.install_thread.isRunning():
|
||||
self.install_thread.terminate()
|
||||
self.install_thread.wait()
|
||||
event.accept()
|
||||
239
jackify/frontends/gui/dialogs/success_dialog.py
Normal file
239
jackify/frontends/gui/dialogs/success_dialog.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Success Dialog
|
||||
|
||||
Celebration dialog shown when workflows complete successfully.
|
||||
Features trophy icon, personalized messaging, and time tracking.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QWidget,
|
||||
QSpacerItem, QSizePolicy, QFrame, QApplication
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QPixmap, QIcon, QFont
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SuccessDialog(QDialog):
|
||||
"""
|
||||
Celebration dialog shown when workflows complete successfully.
|
||||
|
||||
Features:
|
||||
- Trophy icon
|
||||
- Personalized success message
|
||||
- Time taken display
|
||||
- Next steps guidance
|
||||
- Return and Exit buttons
|
||||
"""
|
||||
|
||||
def __init__(self, modlist_name: str, workflow_type: str, time_taken: str, game_name: str = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.modlist_name = modlist_name
|
||||
self.workflow_type = workflow_type
|
||||
self.time_taken = time_taken
|
||||
self.game_name = game_name
|
||||
self.setWindowTitle("Success!")
|
||||
self.setWindowModality(Qt.NonModal)
|
||||
self.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
||||
self.setFixedSize(500, 420)
|
||||
self.setWindowFlag(Qt.WindowDoesNotAcceptFocus, True)
|
||||
self.setStyleSheet("QDialog { background: #181818; color: #fff; border-radius: 12px; }" )
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# --- Card background for content ---
|
||||
card = QFrame(self)
|
||||
card.setObjectName("successCard")
|
||||
card.setFrameShape(QFrame.StyledPanel)
|
||||
card.setFrameShadow(QFrame.Raised)
|
||||
card.setFixedWidth(440)
|
||||
card_layout = QVBoxLayout(card)
|
||||
card_layout.setSpacing(12)
|
||||
card_layout.setContentsMargins(28, 28, 28, 28)
|
||||
card.setStyleSheet(
|
||||
"QFrame#successCard { "
|
||||
" background: #23272e; "
|
||||
" border-radius: 12px; "
|
||||
" border: 1px solid #353a40; "
|
||||
"}"
|
||||
)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
# Trophy icon (smaller, more subtle)
|
||||
trophy_label = QLabel()
|
||||
trophy_label.setAlignment(Qt.AlignCenter)
|
||||
trophy_icon_path = Path(__file__).parent.parent.parent.parent.parent / "Files" / "trophy.png"
|
||||
if trophy_icon_path.exists():
|
||||
pixmap = QPixmap(str(trophy_icon_path)).scaled(36, 36, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
trophy_label.setPixmap(pixmap)
|
||||
else:
|
||||
trophy_label.setText("✓")
|
||||
trophy_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 28px; "
|
||||
" margin-bottom: 4px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(trophy_label)
|
||||
|
||||
# Success title (less saturated green)
|
||||
title_label = QLabel("Success!")
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 22px; "
|
||||
" font-weight: 600; "
|
||||
" color: #2ecc71; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(title_label)
|
||||
|
||||
# Personalized success message (modlist name in Jackify Blue, but less bold)
|
||||
message_text = self._build_success_message()
|
||||
modlist_name_html = f'<span style="color:#3fb7d6; font-size:17px; font-weight:500;">{self.modlist_name}</span>'
|
||||
if self.workflow_type == "install":
|
||||
message_html = f"<span style='font-size:15px;'>{modlist_name_html} installed successfully!</span>"
|
||||
else:
|
||||
message_html = message_text
|
||||
message_label = QLabel(message_html)
|
||||
message_label.setAlignment(Qt.AlignCenter)
|
||||
message_label.setWordWrap(True)
|
||||
message_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 15px; "
|
||||
" color: #e0e0e0; "
|
||||
" line-height: 1.3; "
|
||||
" margin-bottom: 6px; "
|
||||
" max-width: 400px; "
|
||||
" min-width: 200px; "
|
||||
" word-wrap: break-word; "
|
||||
"}"
|
||||
)
|
||||
message_label.setTextFormat(Qt.RichText)
|
||||
card_layout.addWidget(message_label)
|
||||
|
||||
# Time taken
|
||||
time_label = QLabel(f"Completed in {self.time_taken}")
|
||||
time_label.setAlignment(Qt.AlignCenter)
|
||||
time_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 12px; "
|
||||
" color: #b0b0b0; "
|
||||
" font-style: italic; "
|
||||
" margin-bottom: 10px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(time_label)
|
||||
|
||||
# Next steps guidance
|
||||
next_steps_text = self._build_next_steps()
|
||||
next_steps_label = QLabel(next_steps_text)
|
||||
next_steps_label.setAlignment(Qt.AlignCenter)
|
||||
next_steps_label.setWordWrap(True)
|
||||
next_steps_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 13px; "
|
||||
" color: #b0b0b0; "
|
||||
" line-height: 1.2; "
|
||||
" padding: 6px; "
|
||||
" background-color: transparent; "
|
||||
" border-radius: 6px; "
|
||||
" border: none; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(next_steps_label)
|
||||
|
||||
layout.addStretch()
|
||||
layout.addWidget(card, alignment=Qt.AlignCenter)
|
||||
layout.addStretch()
|
||||
|
||||
# Action buttons
|
||||
btn_row = QHBoxLayout()
|
||||
self.return_btn = QPushButton("Return")
|
||||
self.exit_btn = QPushButton("Exit")
|
||||
btn_row.addWidget(self.return_btn)
|
||||
btn_row.addWidget(self.exit_btn)
|
||||
layout.addLayout(btn_row)
|
||||
# Now set up the timer/countdown logic AFTER buttons are created
|
||||
self.return_btn.setEnabled(False)
|
||||
self.exit_btn.setEnabled(False)
|
||||
self._countdown = 3
|
||||
self._orig_return_text = self.return_btn.text()
|
||||
self._timer = QTimer(self)
|
||||
self._timer.timeout.connect(self._update_countdown)
|
||||
self._update_countdown()
|
||||
self._timer.start(1000)
|
||||
self.return_btn.clicked.connect(self.accept)
|
||||
self.exit_btn.clicked.connect(QApplication.quit)
|
||||
|
||||
# Set the Wabbajack icon if available
|
||||
self._set_dialog_icon()
|
||||
|
||||
logger.info(f"SuccessDialog created for {workflow_type}: {modlist_name} (completed in {time_taken})")
|
||||
|
||||
def _set_dialog_icon(self):
|
||||
"""Set the dialog icon to Wabbajack icon if available"""
|
||||
try:
|
||||
# Try to use the same icon as the main application
|
||||
icon_path = Path(__file__).parent.parent.parent.parent.parent / "Files" / "wabbajack-icon.png"
|
||||
if icon_path.exists():
|
||||
icon = QIcon(str(icon_path))
|
||||
self.setWindowIcon(icon)
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not set dialog icon: {e}")
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the dialog user interface"""
|
||||
pass # This method is no longer needed as __init__ handles UI setup
|
||||
|
||||
def _setup_buttons(self, layout):
|
||||
"""Set up the action buttons"""
|
||||
pass # This method is no longer needed as __init__ handles button setup
|
||||
|
||||
def _build_success_message(self) -> str:
|
||||
"""
|
||||
Build the personalized success message based on workflow type.
|
||||
|
||||
Returns:
|
||||
Formatted success message string
|
||||
"""
|
||||
workflow_messages = {
|
||||
"install": f"{self.modlist_name} installed successfully!",
|
||||
"configure_new": f"{self.modlist_name} configured successfully!",
|
||||
"configure_existing": f"{self.modlist_name} configuration updated successfully!",
|
||||
"tuxborn": f"Tuxborn installation completed successfully!",
|
||||
}
|
||||
|
||||
return workflow_messages.get(self.workflow_type, f"{self.modlist_name} completed successfully!")
|
||||
|
||||
def _build_next_steps(self) -> str:
|
||||
"""
|
||||
Build the next steps guidance based on workflow type.
|
||||
|
||||
Returns:
|
||||
Formatted next steps string
|
||||
"""
|
||||
game_display = self.game_name or self.modlist_name
|
||||
if self.workflow_type == "tuxborn":
|
||||
return f"You can now launch Tuxborn from Steam and enjoy your modded {game_display} experience!"
|
||||
else:
|
||||
return f"You can now launch {self.modlist_name} from Steam and enjoy your modded {game_display} experience!"
|
||||
|
||||
def _update_countdown(self):
|
||||
if self._countdown > 0:
|
||||
self.return_btn.setText(f"{self._orig_return_text} ({self._countdown}s)")
|
||||
self.return_btn.setEnabled(False)
|
||||
self.exit_btn.setEnabled(False)
|
||||
self._countdown -= 1
|
||||
else:
|
||||
self.return_btn.setText(self._orig_return_text)
|
||||
self.return_btn.setEnabled(True)
|
||||
self.exit_btn.setEnabled(True)
|
||||
self._timer.stop()
|
||||
529
jackify/frontends/gui/dialogs/ulimit_guidance_dialog.py
Normal file
529
jackify/frontends/gui/dialogs/ulimit_guidance_dialog.py
Normal file
@@ -0,0 +1,529 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ulimit Guidance Dialog
|
||||
|
||||
Provides guidance for manually increasing file descriptor limits when automatic
|
||||
increase fails. Offers distribution-specific instructions and commands.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QTextEdit, QGroupBox, QTabWidget, QWidget, QScrollArea,
|
||||
QFrame, QSizePolicy
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QFont, QIcon
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UlimitGuidanceDialog(QDialog):
|
||||
"""Dialog to provide manual ulimit increase guidance when automatic methods fail"""
|
||||
|
||||
def __init__(self, resource_manager=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.resource_manager = resource_manager
|
||||
self.setWindowTitle("File Descriptor Limit Guidance")
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(800, 600)
|
||||
self.resize(900, 700)
|
||||
|
||||
# Get current status and instructions
|
||||
if self.resource_manager:
|
||||
self.status = self.resource_manager.get_limit_status()
|
||||
self.instructions = self.resource_manager.get_manual_increase_instructions()
|
||||
else:
|
||||
# Fallback if no resource manager provided
|
||||
from jackify.backend.services.resource_manager import ResourceManager
|
||||
temp_manager = ResourceManager()
|
||||
self.status = temp_manager.get_limit_status()
|
||||
self.instructions = temp_manager.get_manual_increase_instructions()
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
# Auto-refresh status every few seconds
|
||||
self.refresh_timer = QTimer()
|
||||
self.refresh_timer.timeout.connect(self._refresh_status)
|
||||
self.refresh_timer.start(3000) # Refresh every 3 seconds
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the user interface"""
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
# Title and current status
|
||||
self._create_header(layout)
|
||||
|
||||
# Main content with tabs
|
||||
self._create_content_tabs(layout)
|
||||
|
||||
# Action buttons
|
||||
self._create_action_buttons(layout)
|
||||
|
||||
# Apply styling
|
||||
self._apply_styling()
|
||||
|
||||
def _create_header(self, layout):
|
||||
"""Create header with current status"""
|
||||
header_frame = QFrame()
|
||||
header_frame.setFrameStyle(QFrame.StyledPanel)
|
||||
header_layout = QVBoxLayout()
|
||||
header_frame.setLayout(header_layout)
|
||||
|
||||
# Title
|
||||
title_label = QLabel("File Descriptor Limit Configuration")
|
||||
title_font = QFont()
|
||||
title_font.setPointSize(14)
|
||||
title_font.setBold(True)
|
||||
title_label.setFont(title_font)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
header_layout.addWidget(title_label)
|
||||
|
||||
# Status information
|
||||
self._create_status_section(header_layout)
|
||||
|
||||
layout.addWidget(header_frame)
|
||||
|
||||
def _create_status_section(self, layout):
|
||||
"""Create current status display"""
|
||||
status_layout = QHBoxLayout()
|
||||
|
||||
# Current limits
|
||||
current_label = QLabel(f"Current Limit: {self.status['current_soft']}")
|
||||
target_label = QLabel(f"Target Limit: {self.status['target_limit']}")
|
||||
max_label = QLabel(f"Maximum Possible: {self.status['max_possible']}")
|
||||
|
||||
# Status indicator
|
||||
if self.status['target_achieved']:
|
||||
status_text = "✓ Optimal"
|
||||
status_color = "#4caf50" # Green
|
||||
elif self.status['can_increase']:
|
||||
status_text = "⚠ Can Improve"
|
||||
status_color = "#ff9800" # Orange
|
||||
else:
|
||||
status_text = "✗ Needs Manual Fix"
|
||||
status_color = "#f44336" # Red
|
||||
|
||||
self.status_label = QLabel(f"Status: {status_text}")
|
||||
self.status_label.setStyleSheet(f"color: {status_color}; font-weight: bold;")
|
||||
|
||||
status_layout.addWidget(current_label)
|
||||
status_layout.addWidget(target_label)
|
||||
status_layout.addWidget(max_label)
|
||||
status_layout.addStretch()
|
||||
status_layout.addWidget(self.status_label)
|
||||
|
||||
layout.addLayout(status_layout)
|
||||
|
||||
def _create_content_tabs(self, layout):
|
||||
"""Create tabbed content with different guidance types"""
|
||||
self.tab_widget = QTabWidget()
|
||||
|
||||
# Quick Fix tab
|
||||
self._create_quick_fix_tab()
|
||||
|
||||
# Permanent Fix tab
|
||||
self._create_permanent_fix_tab()
|
||||
|
||||
# Troubleshooting tab
|
||||
self._create_troubleshooting_tab()
|
||||
|
||||
layout.addWidget(self.tab_widget)
|
||||
|
||||
def _create_quick_fix_tab(self):
|
||||
"""Create quick/temporary fix tab"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
|
||||
# Explanation
|
||||
explanation = QLabel(
|
||||
"Quick fixes apply only to the current terminal session. "
|
||||
"You'll need to run these commands each time you start Jackify from a new terminal."
|
||||
)
|
||||
explanation.setWordWrap(True)
|
||||
explanation.setStyleSheet("color: #666; font-style: italic; margin-bottom: 10px;")
|
||||
layout.addWidget(explanation)
|
||||
|
||||
# Commands group
|
||||
commands_group = QGroupBox("Commands to Run")
|
||||
commands_layout = QVBoxLayout()
|
||||
commands_group.setLayout(commands_layout)
|
||||
|
||||
# Command text
|
||||
if 'temporary' in self.instructions['methods']:
|
||||
temp_method = self.instructions['methods']['temporary']
|
||||
|
||||
commands_text = QTextEdit()
|
||||
commands_text.setPlainText('\n'.join(temp_method['commands']))
|
||||
commands_text.setMaximumHeight(120)
|
||||
commands_text.setFont(QFont("monospace"))
|
||||
commands_layout.addWidget(commands_text)
|
||||
|
||||
# Note
|
||||
if 'note' in temp_method:
|
||||
note_label = QLabel(f"Note: {temp_method['note']}")
|
||||
note_label.setWordWrap(True)
|
||||
note_label.setStyleSheet("color: #666; font-style: italic;")
|
||||
commands_layout.addWidget(note_label)
|
||||
|
||||
layout.addWidget(commands_group)
|
||||
|
||||
# Current session test
|
||||
test_group = QGroupBox("Test Current Session")
|
||||
test_layout = QVBoxLayout()
|
||||
test_group.setLayout(test_layout)
|
||||
|
||||
test_label = QLabel("You can test if the commands worked by running:")
|
||||
test_layout.addWidget(test_label)
|
||||
|
||||
test_command = QTextEdit()
|
||||
test_command.setPlainText("ulimit -n")
|
||||
test_command.setMaximumHeight(40)
|
||||
test_command.setFont(QFont("monospace"))
|
||||
test_layout.addWidget(test_command)
|
||||
|
||||
expected_label = QLabel(f"Expected result: {self.instructions['target_limit']} or higher")
|
||||
expected_label.setStyleSheet("color: #666;")
|
||||
test_layout.addWidget(expected_label)
|
||||
|
||||
layout.addWidget(test_group)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
self.tab_widget.addTab(widget, "Quick Fix")
|
||||
|
||||
def _create_permanent_fix_tab(self):
|
||||
"""Create permanent fix tab"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
|
||||
# Explanation
|
||||
explanation = QLabel(
|
||||
"Permanent fixes modify system configuration files and require administrator privileges. "
|
||||
"Changes take effect after logout/login or system reboot."
|
||||
)
|
||||
explanation.setWordWrap(True)
|
||||
explanation.setStyleSheet("color: #666; font-style: italic; margin-bottom: 10px;")
|
||||
layout.addWidget(explanation)
|
||||
|
||||
# Distribution detection
|
||||
distro_label = QLabel(f"Detected Distribution: {self.instructions['distribution'].title()}")
|
||||
distro_label.setStyleSheet("font-weight: bold; color: #333;")
|
||||
layout.addWidget(distro_label)
|
||||
|
||||
# Commands group
|
||||
commands_group = QGroupBox("System Configuration Commands")
|
||||
commands_layout = QVBoxLayout()
|
||||
commands_group.setLayout(commands_layout)
|
||||
|
||||
# Warning
|
||||
warning_label = QLabel(
|
||||
"⚠️ WARNING: These commands require root/sudo privileges and modify system files. "
|
||||
"Make sure you understand what each command does before running it."
|
||||
)
|
||||
warning_label.setWordWrap(True)
|
||||
warning_label.setStyleSheet("background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 8px; border-radius: 4px; color: #856404;")
|
||||
commands_layout.addWidget(warning_label)
|
||||
|
||||
# Command text
|
||||
if 'permanent' in self.instructions['methods']:
|
||||
perm_method = self.instructions['methods']['permanent']
|
||||
|
||||
commands_text = QTextEdit()
|
||||
commands_text.setPlainText('\n'.join(perm_method['commands']))
|
||||
commands_text.setMinimumHeight(200)
|
||||
commands_text.setFont(QFont("monospace"))
|
||||
commands_layout.addWidget(commands_text)
|
||||
|
||||
# Note
|
||||
if 'note' in perm_method:
|
||||
note_label = QLabel(f"Note: {perm_method['note']}")
|
||||
note_label.setWordWrap(True)
|
||||
note_label.setStyleSheet("color: #666; font-style: italic;")
|
||||
commands_layout.addWidget(note_label)
|
||||
|
||||
layout.addWidget(commands_group)
|
||||
|
||||
# Verification group
|
||||
verify_group = QGroupBox("Verification After Reboot/Re-login")
|
||||
verify_layout = QVBoxLayout()
|
||||
verify_group.setLayout(verify_layout)
|
||||
|
||||
verify_label = QLabel("After rebooting or logging out and back in, verify the change:")
|
||||
verify_layout.addWidget(verify_label)
|
||||
|
||||
verify_command = QTextEdit()
|
||||
verify_command.setPlainText("ulimit -n")
|
||||
verify_command.setMaximumHeight(40)
|
||||
verify_command.setFont(QFont("monospace"))
|
||||
verify_layout.addWidget(verify_command)
|
||||
|
||||
expected_label = QLabel(f"Expected result: {self.instructions['target_limit']} or higher")
|
||||
expected_label.setStyleSheet("color: #666;")
|
||||
verify_layout.addWidget(expected_label)
|
||||
|
||||
layout.addWidget(verify_group)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
self.tab_widget.addTab(widget, "Permanent Fix")
|
||||
|
||||
def _create_troubleshooting_tab(self):
|
||||
"""Create troubleshooting tab"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
|
||||
# Create scrollable area for troubleshooting content
|
||||
scroll = QScrollArea()
|
||||
scroll_widget = QWidget()
|
||||
scroll_layout = QVBoxLayout()
|
||||
scroll_widget.setLayout(scroll_layout)
|
||||
|
||||
# Common issues
|
||||
issues_group = QGroupBox("Common Issues and Solutions")
|
||||
issues_layout = QVBoxLayout()
|
||||
issues_group.setLayout(issues_layout)
|
||||
|
||||
issues_text = """
|
||||
<b>Issue:</b> "Operation not permitted" when trying to increase limits<br>
|
||||
<b>Solution:</b> You may need root privileges or the hard limit may be too low. Try the permanent fix method.
|
||||
|
||||
<b>Issue:</b> Changes don't persist after closing terminal<br>
|
||||
<b>Solution:</b> Use the permanent fix method to modify system configuration files.
|
||||
|
||||
<b>Issue:</b> Still getting "too many open files" errors after increasing limits<br>
|
||||
<b>Solution:</b> Some applications may need to be restarted to pick up the new limits. Try restarting Jackify.
|
||||
|
||||
<b>Issue:</b> Can't increase above a certain number<br>
|
||||
<b>Solution:</b> The hard limit may be set by system administrator or systemd. Check systemd service limits if applicable.
|
||||
"""
|
||||
|
||||
issues_label = QLabel(issues_text)
|
||||
issues_label.setWordWrap(True)
|
||||
issues_label.setTextFormat(Qt.RichText)
|
||||
issues_layout.addWidget(issues_label)
|
||||
|
||||
scroll_layout.addWidget(issues_group)
|
||||
|
||||
# System information
|
||||
sysinfo_group = QGroupBox("System Information")
|
||||
sysinfo_layout = QVBoxLayout()
|
||||
sysinfo_group.setLayout(sysinfo_layout)
|
||||
|
||||
sysinfo_text = f"""
|
||||
<b>Current Soft Limit:</b> {self.status['current_soft']}<br>
|
||||
<b>Current Hard Limit:</b> {self.status['current_hard']}<br>
|
||||
<b>Target Limit:</b> {self.status['target_limit']}<br>
|
||||
<b>Detected Distribution:</b> {self.instructions['distribution']}<br>
|
||||
<b>Can Increase Automatically:</b> {"Yes" if self.status['can_increase'] else "No"}<br>
|
||||
<b>Target Achieved:</b> {"Yes" if self.status['target_achieved'] else "No"}
|
||||
"""
|
||||
|
||||
sysinfo_label = QLabel(sysinfo_text)
|
||||
sysinfo_label.setWordWrap(True)
|
||||
sysinfo_label.setTextFormat(Qt.RichText)
|
||||
sysinfo_label.setFont(QFont("monospace", 9))
|
||||
sysinfo_layout.addWidget(sysinfo_label)
|
||||
|
||||
scroll_layout.addWidget(sysinfo_group)
|
||||
|
||||
# Additional resources
|
||||
resources_group = QGroupBox("Additional Resources")
|
||||
resources_layout = QVBoxLayout()
|
||||
resources_group.setLayout(resources_layout)
|
||||
|
||||
resources_text = """
|
||||
<b>For more help:</b><br>
|
||||
• Check your distribution's documentation for ulimit configuration<br>
|
||||
• Search for "increase file descriptor limit [your_distribution]"<br>
|
||||
• Consider asking on your distribution's support forums<br>
|
||||
• Jackify documentation and issue tracker on GitHub
|
||||
"""
|
||||
|
||||
resources_label = QLabel(resources_text)
|
||||
resources_label.setWordWrap(True)
|
||||
resources_label.setTextFormat(Qt.RichText)
|
||||
resources_layout.addWidget(resources_label)
|
||||
|
||||
scroll_layout.addWidget(resources_group)
|
||||
|
||||
scroll_layout.addStretch()
|
||||
|
||||
scroll.setWidget(scroll_widget)
|
||||
scroll.setWidgetResizable(True)
|
||||
layout.addWidget(scroll)
|
||||
|
||||
self.tab_widget.addTab(widget, "Troubleshooting")
|
||||
|
||||
def _create_action_buttons(self, layout):
|
||||
"""Create action buttons"""
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
# Try Again button
|
||||
self.try_again_btn = QPushButton("Try Automatic Fix Again")
|
||||
self.try_again_btn.clicked.connect(self._try_automatic_fix)
|
||||
self.try_again_btn.setEnabled(self.status['can_increase'] and not self.status['target_achieved'])
|
||||
|
||||
# Refresh Status button
|
||||
refresh_btn = QPushButton("Refresh Status")
|
||||
refresh_btn.clicked.connect(self._refresh_status)
|
||||
|
||||
# Close button
|
||||
close_btn = QPushButton("Close")
|
||||
close_btn.clicked.connect(self.accept)
|
||||
close_btn.setDefault(True)
|
||||
|
||||
button_layout.addWidget(self.try_again_btn)
|
||||
button_layout.addWidget(refresh_btn)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(close_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def _apply_styling(self):
|
||||
"""Apply dialog styling"""
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
QGroupBox {
|
||||
font-weight: bold;
|
||||
border: 2px solid #cccccc;
|
||||
border-radius: 5px;
|
||||
margin-top: 1ex;
|
||||
padding-top: 10px;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
QTextEdit {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #004175;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
}
|
||||
""")
|
||||
|
||||
def _try_automatic_fix(self):
|
||||
"""Try automatic fix again"""
|
||||
if self.resource_manager:
|
||||
success = self.resource_manager.apply_recommended_limits()
|
||||
if success:
|
||||
self._refresh_status()
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
MessageService.information(
|
||||
self,
|
||||
"Success",
|
||||
"File descriptor limits have been increased successfully!",
|
||||
safety_level="low"
|
||||
)
|
||||
else:
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
MessageService.warning(
|
||||
self,
|
||||
"Fix Failed",
|
||||
"Automatic fix failed. Please try the manual methods shown in the tabs above.",
|
||||
safety_level="medium"
|
||||
)
|
||||
|
||||
def _refresh_status(self):
|
||||
"""Refresh current status display"""
|
||||
try:
|
||||
if self.resource_manager:
|
||||
self.status = self.resource_manager.get_limit_status()
|
||||
else:
|
||||
from jackify.backend.services.resource_manager import ResourceManager
|
||||
temp_manager = ResourceManager()
|
||||
self.status = temp_manager.get_limit_status()
|
||||
|
||||
# Update status display in header
|
||||
header_frame = self.layout().itemAt(0).widget()
|
||||
if header_frame:
|
||||
# Find and update status section
|
||||
header_layout = header_frame.layout()
|
||||
status_layout = header_layout.itemAt(1).layout()
|
||||
|
||||
# Update individual labels
|
||||
status_layout.itemAt(0).widget().setText(f"Current Limit: {self.status['current_soft']}")
|
||||
status_layout.itemAt(1).widget().setText(f"Target Limit: {self.status['target_limit']}")
|
||||
status_layout.itemAt(2).widget().setText(f"Maximum Possible: {self.status['max_possible']}")
|
||||
|
||||
# Update status indicator
|
||||
if self.status['target_achieved']:
|
||||
status_text = "✓ Optimal"
|
||||
status_color = "#4caf50" # Green
|
||||
elif self.status['can_increase']:
|
||||
status_text = "⚠ Can Improve"
|
||||
status_color = "#ff9800" # Orange
|
||||
else:
|
||||
status_text = "✗ Needs Manual Fix"
|
||||
status_color = "#f44336" # Red
|
||||
|
||||
self.status_label.setText(f"Status: {status_text}")
|
||||
self.status_label.setStyleSheet(f"color: {status_color}; font-weight: bold;")
|
||||
|
||||
# Update try again button
|
||||
self.try_again_btn.setEnabled(self.status['can_increase'] and not self.status['target_achieved'])
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error refreshing status: {e}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle dialog close event"""
|
||||
if hasattr(self, 'refresh_timer'):
|
||||
self.refresh_timer.stop()
|
||||
event.accept()
|
||||
|
||||
|
||||
# Convenience function for easy use
|
||||
def show_ulimit_guidance(parent=None, resource_manager=None):
|
||||
"""
|
||||
Show the ulimit guidance dialog
|
||||
|
||||
Args:
|
||||
parent: Parent widget for the dialog
|
||||
resource_manager: Optional ResourceManager instance
|
||||
|
||||
Returns:
|
||||
Dialog result (QDialog.Accepted or QDialog.Rejected)
|
||||
"""
|
||||
dialog = UlimitGuidanceDialog(resource_manager, parent)
|
||||
return dialog.exec()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test the dialog
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Create and show dialog
|
||||
result = show_ulimit_guidance()
|
||||
|
||||
sys.exit(result)
|
||||
188
jackify/frontends/gui/dialogs/warning_dialog.py
Normal file
188
jackify/frontends/gui/dialogs/warning_dialog.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Warning Dialog
|
||||
|
||||
Custom warning dialog for destructive actions (e.g., deleting directory contents).
|
||||
Matches Jackify theming and requires explicit user confirmation.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QFrame, QSizePolicy, QTextEdit
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QPixmap, QIcon, QFont
|
||||
from .. import shared_theme
|
||||
|
||||
class WarningDialog(QDialog):
|
||||
"""
|
||||
Jackify-themed warning dialog for dangerous/destructive actions.
|
||||
Requires user to type 'DELETE' to confirm.
|
||||
"""
|
||||
def __init__(self, warning_message: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Warning!")
|
||||
self.setModal(True)
|
||||
# Increased height for better text display, scalable for 800p screens
|
||||
self.setFixedSize(500, 440)
|
||||
self.confirmed = False
|
||||
self._setup_ui(warning_message)
|
||||
|
||||
def _setup_ui(self, warning_message):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Card background
|
||||
card = QFrame(self)
|
||||
card.setObjectName("warningCard")
|
||||
card.setFrameShape(QFrame.StyledPanel)
|
||||
card.setFrameShadow(QFrame.Raised)
|
||||
card.setMinimumWidth(440)
|
||||
card.setMinimumHeight(320)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
card_layout = QVBoxLayout(card)
|
||||
card_layout.setSpacing(16)
|
||||
card_layout.setContentsMargins(28, 28, 28, 28)
|
||||
card.setStyleSheet(
|
||||
"QFrame#warningCard { "
|
||||
" background: #2d2323; "
|
||||
" border-radius: 12px; "
|
||||
" border: 2px solid #e67e22; "
|
||||
"}"
|
||||
)
|
||||
|
||||
# Warning icon
|
||||
icon_label = QLabel()
|
||||
icon_label.setAlignment(Qt.AlignCenter)
|
||||
icon_label.setText("!")
|
||||
icon_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 36px; "
|
||||
" font-weight: bold; "
|
||||
" color: #e67e22; "
|
||||
" margin-bottom: 4px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(icon_label)
|
||||
|
||||
# Warning title
|
||||
title_label = QLabel("Potentially Destructive Action!")
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 20px; "
|
||||
" font-weight: 600; "
|
||||
" color: #e67e22; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(title_label)
|
||||
|
||||
# Warning message (use a scrollable text area for long messages)
|
||||
message_text = QTextEdit()
|
||||
message_text.setReadOnly(True)
|
||||
message_text.setPlainText(warning_message)
|
||||
message_text.setMinimumHeight(80)
|
||||
message_text.setMaximumHeight(160)
|
||||
message_text.setStyleSheet(
|
||||
"QTextEdit { "
|
||||
" font-size: 15px; "
|
||||
" color: #e0e0e0; "
|
||||
" background: transparent; "
|
||||
" border: none; "
|
||||
" line-height: 1.3; "
|
||||
" margin-bottom: 6px; "
|
||||
" max-width: 400px; "
|
||||
" min-width: 200px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(message_text)
|
||||
|
||||
# Confirmation entry
|
||||
confirm_label = QLabel("Type 'DELETE' to confirm:")
|
||||
confirm_label.setAlignment(Qt.AlignCenter)
|
||||
confirm_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 13px; "
|
||||
" color: #e67e22; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(confirm_label)
|
||||
|
||||
self.confirm_edit = QLineEdit()
|
||||
self.confirm_edit.setAlignment(Qt.AlignCenter)
|
||||
self.confirm_edit.setPlaceholderText("DELETE")
|
||||
self.confirm_edit.setStyleSheet(
|
||||
"QLineEdit { "
|
||||
" font-size: 15px; "
|
||||
" border: 1px solid #e67e22; "
|
||||
" border-radius: 6px; "
|
||||
" padding: 6px; "
|
||||
" background: #23272e; "
|
||||
" color: #e67e22; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(self.confirm_edit)
|
||||
|
||||
# Action buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(12)
|
||||
button_layout.addStretch()
|
||||
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.setFixedSize(120, 36)
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
cancel_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #95a5a6; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
" font-weight: bold; "
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #7f8c8d; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #6c7b7d; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(cancel_btn)
|
||||
|
||||
confirm_btn = QPushButton("Proceed")
|
||||
confirm_btn.setFixedSize(120, 36)
|
||||
confirm_btn.clicked.connect(self._on_confirm)
|
||||
confirm_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #e67e22; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
" font-weight: bold; "
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #d35400; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #b34700; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(confirm_btn)
|
||||
button_layout.addStretch()
|
||||
card_layout.addLayout(button_layout)
|
||||
|
||||
layout.addStretch()
|
||||
layout.addWidget(card, alignment=Qt.AlignCenter)
|
||||
layout.addStretch()
|
||||
|
||||
def _on_confirm(self):
|
||||
if self.confirm_edit.text().strip().upper() == "DELETE":
|
||||
self.confirmed = True
|
||||
self.accept()
|
||||
else:
|
||||
self.confirm_edit.setText("")
|
||||
self.confirm_edit.setPlaceholderText("Type DELETE to confirm")
|
||||
self.confirm_edit.setStyleSheet(self.confirm_edit.styleSheet() + "QLineEdit { background: #3b2323; }")
|
||||
Reference in New Issue
Block a user