Files
Jackify/jackify/frontends/gui/services/message_service.py
2025-11-04 12:54:15 +00:00

289 lines
12 KiB
Python

"""
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.setTextFormat(Qt.RichText)
msg_box.setTextInteractionFlags(Qt.TextBrowserInteraction)
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()