mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01: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:
287
jackify/frontends/gui/services/message_service.py
Normal file
287
jackify/frontends/gui/services/message_service.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
Non-Focus-Stealing Message Service for Jackify
|
||||
Provides message boxes that don't steal focus from the current application
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
from typing import Optional
|
||||
from PySide6.QtWidgets import QMessageBox, QWidget, QLineEdit, QLabel, QVBoxLayout, QHBoxLayout, QCheckBox
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
|
||||
|
||||
class NonFocusMessageBox(QMessageBox):
|
||||
"""Custom QMessageBox that prevents focus stealing"""
|
||||
|
||||
def __init__(self, parent=None, critical=False, safety_level="low"):
|
||||
super().__init__(parent)
|
||||
self.safety_level = safety_level
|
||||
self._setup_no_focus_attributes(critical, safety_level)
|
||||
|
||||
def _setup_no_focus_attributes(self, critical, safety_level):
|
||||
"""Configure the message box to not steal focus"""
|
||||
# Set modality based on criticality and safety level
|
||||
if critical or safety_level == "high":
|
||||
self.setWindowModality(Qt.ApplicationModal)
|
||||
elif safety_level == "medium":
|
||||
self.setWindowModality(Qt.NonModal)
|
||||
else:
|
||||
self.setWindowModality(Qt.NonModal)
|
||||
|
||||
# Prevent focus stealing
|
||||
self.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
||||
self.setWindowFlags(
|
||||
self.windowFlags() |
|
||||
Qt.WindowStaysOnTopHint |
|
||||
Qt.WindowDoesNotAcceptFocus
|
||||
)
|
||||
|
||||
# Set focus policy to prevent taking focus
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
# Make sure child widgets don't steal focus either
|
||||
for child in self.findChildren(QWidget):
|
||||
child.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Override to ensure no focus stealing on show"""
|
||||
super().showEvent(event)
|
||||
# Ensure we don't steal focus
|
||||
self.activateWindow()
|
||||
self.raise_()
|
||||
|
||||
|
||||
class SafeMessageBox(NonFocusMessageBox):
|
||||
"""Enhanced message box with safety features"""
|
||||
|
||||
def __init__(self, parent=None, safety_level="low"):
|
||||
super().__init__(parent, critical=(safety_level == "high"), safety_level=safety_level)
|
||||
self.safety_level = safety_level
|
||||
self.countdown_remaining = 0
|
||||
self.confirmation_code = None
|
||||
self.countdown_timer = None
|
||||
self.code_input = None
|
||||
self.understanding_checkbox = None
|
||||
|
||||
def setup_safety_features(self, title: str, message: str,
|
||||
danger_action: str = "OK",
|
||||
safe_action: str = "Cancel",
|
||||
is_question: bool = False):
|
||||
self.setWindowTitle(title)
|
||||
self.setText(message)
|
||||
if self.safety_level == "high":
|
||||
self.setIcon(QMessageBox.Warning)
|
||||
self._setup_high_safety(danger_action, safe_action)
|
||||
elif self.safety_level == "medium":
|
||||
self.setIcon(QMessageBox.Information)
|
||||
self._setup_medium_safety(danger_action, safe_action)
|
||||
else:
|
||||
self.setIcon(QMessageBox.Information)
|
||||
self._setup_low_safety(danger_action, safe_action)
|
||||
# --- Fix: For question dialogs, set proceed/cancel button return values, but do NOT call setStandardButtons ---
|
||||
if is_question and hasattr(self, 'proceed_btn'):
|
||||
self.proceed_btn.setText(danger_action)
|
||||
self.proceed_btn.setProperty('role', QMessageBox.YesRole)
|
||||
self.proceed_btn.clicked.disconnect()
|
||||
self.proceed_btn.clicked.connect(lambda: self.done(QMessageBox.Yes))
|
||||
self.cancel_btn.setText(safe_action)
|
||||
self.cancel_btn.setProperty('role', QMessageBox.NoRole)
|
||||
self.cancel_btn.clicked.disconnect()
|
||||
self.cancel_btn.clicked.connect(lambda: self.done(QMessageBox.No))
|
||||
|
||||
def _setup_high_safety(self, danger_action: str, safe_action: str):
|
||||
"""High safety: requires typing confirmation code"""
|
||||
# Generate random confirmation code
|
||||
self.confirmation_code = ''.join(random.choices(string.ascii_uppercase, k=6))
|
||||
|
||||
# Create custom buttons
|
||||
self.proceed_btn = self.addButton(danger_action, QMessageBox.AcceptRole)
|
||||
self.cancel_btn = self.addButton(safe_action, QMessageBox.RejectRole)
|
||||
|
||||
# Make cancel the default (Enter key)
|
||||
self.setDefaultButton(self.cancel_btn)
|
||||
|
||||
# Initially disable proceed button
|
||||
self.proceed_btn.setEnabled(False)
|
||||
|
||||
# Add confirmation code input
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
|
||||
instruction = QLabel(f"Type '{self.confirmation_code}' to confirm:")
|
||||
instruction.setStyleSheet("font-weight: bold; color: red;")
|
||||
layout.addWidget(instruction)
|
||||
|
||||
self.code_input = QLineEdit()
|
||||
self.code_input.setPlaceholderText("Enter confirmation code...")
|
||||
self.code_input.textChanged.connect(self._check_code_input)
|
||||
layout.addWidget(self.code_input)
|
||||
|
||||
self.layout().addWidget(widget, 1, 0, 1, self.layout().columnCount())
|
||||
|
||||
# Start countdown
|
||||
self._start_countdown(3)
|
||||
|
||||
def _setup_medium_safety(self, danger_action: str, safe_action: str):
|
||||
"""Medium safety: requires wait period"""
|
||||
# Create custom buttons
|
||||
self.proceed_btn = self.addButton(danger_action, QMessageBox.AcceptRole)
|
||||
self.cancel_btn = self.addButton(safe_action, QMessageBox.RejectRole)
|
||||
|
||||
# Make cancel the default (Enter key)
|
||||
self.setDefaultButton(self.cancel_btn)
|
||||
|
||||
# Initially disable proceed button
|
||||
self.proceed_btn.setEnabled(False)
|
||||
|
||||
# Start countdown
|
||||
self._start_countdown(3)
|
||||
|
||||
def _setup_low_safety(self, danger_action: str, safe_action: str):
|
||||
"""Low safety: no additional features needed"""
|
||||
# Create standard buttons
|
||||
self.proceed_btn = self.addButton(danger_action, QMessageBox.AcceptRole)
|
||||
self.cancel_btn = self.addButton(safe_action, QMessageBox.RejectRole)
|
||||
|
||||
# Make proceed the default for low safety
|
||||
self.setDefaultButton(self.proceed_btn)
|
||||
|
||||
def _start_countdown(self, seconds: int):
|
||||
self.countdown_timer = QTimer()
|
||||
self.countdown_timer.timeout.connect(self._update_countdown)
|
||||
self.countdown_remaining = seconds
|
||||
self._update_countdown()
|
||||
self.countdown_timer.start(1000) # Update every second
|
||||
|
||||
def _update_countdown(self):
|
||||
if self.countdown_remaining > 0:
|
||||
if hasattr(self, 'proceed_btn'):
|
||||
if self.safety_level == "high":
|
||||
self.proceed_btn.setText(f"Please wait {self.countdown_remaining}s...")
|
||||
else:
|
||||
self.proceed_btn.setText(f"OK ({self.countdown_remaining}s)")
|
||||
self.proceed_btn.setEnabled(False)
|
||||
if hasattr(self, 'cancel_btn'):
|
||||
self.cancel_btn.setEnabled(False)
|
||||
self.countdown_remaining -= 1
|
||||
else:
|
||||
self.countdown_timer.stop()
|
||||
if hasattr(self, 'proceed_btn'):
|
||||
if self.safety_level == "high":
|
||||
self.proceed_btn.setText("Proceed")
|
||||
else:
|
||||
self.proceed_btn.setText("OK")
|
||||
self.proceed_btn.setEnabled(True)
|
||||
if hasattr(self, 'cancel_btn'):
|
||||
self.cancel_btn.setEnabled(True)
|
||||
self._check_all_requirements()
|
||||
|
||||
def _check_code_input(self):
|
||||
"""Check if typed code matches"""
|
||||
if self.countdown_remaining <= 0:
|
||||
self._check_all_requirements()
|
||||
|
||||
def _check_all_requirements(self):
|
||||
"""Check if all requirements are met"""
|
||||
can_proceed = self.countdown_remaining <= 0
|
||||
|
||||
if self.safety_level == "high":
|
||||
can_proceed = can_proceed and (
|
||||
self.code_input.text().upper() == self.confirmation_code
|
||||
)
|
||||
|
||||
self.proceed_btn.setEnabled(can_proceed)
|
||||
|
||||
|
||||
class MessageService:
|
||||
"""Service class for creating non-focus-stealing message boxes"""
|
||||
|
||||
@staticmethod
|
||||
def _create_base_message_box(parent: Optional[QWidget] = None, critical: bool = False, safety_level: str = "low") -> NonFocusMessageBox:
|
||||
"""Create a base message box with no focus stealing"""
|
||||
if safety_level in ["medium", "high"]:
|
||||
return SafeMessageBox(parent, safety_level)
|
||||
else:
|
||||
return NonFocusMessageBox(parent, critical)
|
||||
|
||||
@staticmethod
|
||||
def information(parent: Optional[QWidget] = None,
|
||||
title: str = "Information",
|
||||
message: str = "",
|
||||
buttons: QMessageBox.StandardButtons = QMessageBox.Ok,
|
||||
default_button: QMessageBox.StandardButton = QMessageBox.Ok,
|
||||
critical: bool = False,
|
||||
safety_level: str = "low") -> int:
|
||||
"""Show information message without stealing focus"""
|
||||
if safety_level in ["medium", "high"]:
|
||||
msg_box = SafeMessageBox(parent, safety_level)
|
||||
msg_box.setup_safety_features(title, message, "OK", "Cancel")
|
||||
else:
|
||||
msg_box = MessageService._create_base_message_box(parent, critical, safety_level)
|
||||
msg_box.setIcon(QMessageBox.Information)
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setText(message)
|
||||
msg_box.setStandardButtons(buttons)
|
||||
msg_box.setDefaultButton(default_button)
|
||||
|
||||
return msg_box.exec()
|
||||
|
||||
@staticmethod
|
||||
def warning(parent: Optional[QWidget] = None,
|
||||
title: str = "Warning",
|
||||
message: str = "",
|
||||
buttons: QMessageBox.StandardButtons = QMessageBox.Ok,
|
||||
default_button: QMessageBox.StandardButton = QMessageBox.Ok,
|
||||
critical: bool = False,
|
||||
safety_level: str = "low") -> int:
|
||||
"""Show warning message without stealing focus"""
|
||||
if safety_level in ["medium", "high"]:
|
||||
msg_box = SafeMessageBox(parent, safety_level)
|
||||
msg_box.setup_safety_features(title, message, "OK", "Cancel")
|
||||
else:
|
||||
msg_box = MessageService._create_base_message_box(parent, critical, safety_level)
|
||||
msg_box.setIcon(QMessageBox.Warning)
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setText(message)
|
||||
msg_box.setStandardButtons(buttons)
|
||||
msg_box.setDefaultButton(default_button)
|
||||
|
||||
return msg_box.exec()
|
||||
|
||||
@staticmethod
|
||||
def critical(parent: Optional[QWidget] = None,
|
||||
title: str = "Critical Error",
|
||||
message: str = "",
|
||||
buttons: QMessageBox.StandardButtons = QMessageBox.Ok,
|
||||
default_button: QMessageBox.StandardButton = QMessageBox.Ok,
|
||||
safety_level: str = "medium") -> int:
|
||||
"""Show critical error message (always requires attention)"""
|
||||
msg_box = MessageService._create_base_message_box(parent, critical=True, safety_level=safety_level)
|
||||
msg_box.setIcon(QMessageBox.Critical)
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setText(message)
|
||||
msg_box.setStandardButtons(buttons)
|
||||
msg_box.setDefaultButton(default_button)
|
||||
return msg_box.exec()
|
||||
|
||||
@staticmethod
|
||||
def question(parent: Optional[QWidget] = None,
|
||||
title: str = "Question",
|
||||
message: str = "",
|
||||
buttons: QMessageBox.StandardButtons = QMessageBox.Yes | QMessageBox.No,
|
||||
default_button: QMessageBox.StandardButton = QMessageBox.No,
|
||||
critical: bool = False,
|
||||
safety_level: str = "low") -> int:
|
||||
"""Show question dialog without stealing focus"""
|
||||
if safety_level in ["medium", "high"]:
|
||||
msg_box = SafeMessageBox(parent, safety_level)
|
||||
msg_box.setup_safety_features(title, message, "Yes", "No", is_question=True)
|
||||
else:
|
||||
msg_box = MessageService._create_base_message_box(parent, critical, safety_level)
|
||||
msg_box.setIcon(QMessageBox.Question)
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setText(message)
|
||||
msg_box.setStandardButtons(buttons)
|
||||
msg_box.setDefaultButton(default_button)
|
||||
|
||||
return msg_box.exec()
|
||||
Reference in New Issue
Block a user