127 lines
4.1 KiB
Python
127 lines
4.1 KiB
Python
"""
|
|
Accessible text display dialog - single point of truth for showing text to screen reader users
|
|
Based on the implementation from Bifrost
|
|
"""
|
|
|
|
import time
|
|
|
|
from PySide6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox
|
|
)
|
|
from PySide6.QtCore import Qt
|
|
|
|
|
|
class AccessibleTextDialog(QDialog):
|
|
"""
|
|
Single reusable dialog for displaying text content accessibly to screen readers.
|
|
Provides keyboard navigation, text selection, and proper focus management.
|
|
"""
|
|
|
|
# Class-level error debouncing to prevent dialog spam
|
|
_lastErrorKey: str = ""
|
|
_lastErrorTime: float = 0.0
|
|
_errorDebounceSeconds: float = 2.0
|
|
|
|
def __init__(self, title: str, content: str, dialogType: str = "info", parent=None):
|
|
"""
|
|
Initialize accessible text dialog
|
|
|
|
Args:
|
|
title: Window title
|
|
content: Text content to display
|
|
dialogType: "info", "error", "success", or "warning" - affects accessible name
|
|
parent: Parent widget
|
|
"""
|
|
super().__init__(parent)
|
|
self.setWindowTitle(title)
|
|
self.setModal(True)
|
|
|
|
# Size based on content length
|
|
if len(content) > 500:
|
|
self.setMinimumSize(600, 400)
|
|
elif len(content) > 200:
|
|
self.setMinimumSize(500, 300)
|
|
else:
|
|
self.setMinimumSize(400, 200)
|
|
|
|
self.setupUi(content, dialogType)
|
|
|
|
def setupUi(self, content: str, dialogType: str):
|
|
"""Setup the dialog UI with accessible text widget"""
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Create accessible text edit widget
|
|
self.textEdit = QTextEdit()
|
|
self.textEdit.setPlainText(content)
|
|
self.textEdit.setReadOnly(True)
|
|
|
|
# Set accessible name based on dialog type
|
|
accessibleNames = {
|
|
"info": "Information Text",
|
|
"error": "Error Details",
|
|
"warning": "Warning Information",
|
|
"success": "Success Information"
|
|
}
|
|
self.textEdit.setAccessibleName(accessibleNames.get(dialogType, "Dialog Text"))
|
|
|
|
# Enable keyboard text selection and navigation
|
|
self.textEdit.setTextInteractionFlags(
|
|
Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse
|
|
)
|
|
|
|
layout.addWidget(self.textEdit)
|
|
|
|
# Button box
|
|
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
|
|
buttonBox.accepted.connect(self.accept)
|
|
layout.addWidget(buttonBox)
|
|
|
|
# Focus the text edit first so user can immediately read content
|
|
self.textEdit.setFocus()
|
|
|
|
@classmethod
|
|
def showInfo(cls, title: str, content: str, parent=None):
|
|
"""Convenience method for info dialogs"""
|
|
dialog = cls(title, content, "info", parent)
|
|
dialog.exec()
|
|
|
|
@classmethod
|
|
def showError(cls, title: str, message: str, details: str = None, parent=None):
|
|
"""Convenience method for error dialogs with optional details.
|
|
|
|
Includes debouncing to prevent the same error from spawning multiple dialogs
|
|
in rapid succession.
|
|
"""
|
|
# Debounce: skip if same error within debounce window
|
|
errorKey = f"{title}:{message}"
|
|
currentTime = time.monotonic()
|
|
if (errorKey == cls._lastErrorKey and
|
|
currentTime - cls._lastErrorTime < cls._errorDebounceSeconds):
|
|
return
|
|
|
|
cls._lastErrorKey = errorKey
|
|
cls._lastErrorTime = currentTime
|
|
|
|
if details:
|
|
content = f"{message}\n\nError Details:\n{details}"
|
|
else:
|
|
content = message
|
|
dialog = cls(title, content, "error", parent)
|
|
dialog.exec()
|
|
|
|
@classmethod
|
|
def showSuccess(cls, title: str, message: str, details: str = None, parent=None):
|
|
"""Convenience method for success dialogs with optional details"""
|
|
if details:
|
|
content = f"{message}\n\n{details}"
|
|
else:
|
|
content = message
|
|
dialog = cls(title, content, "success", parent)
|
|
dialog.exec()
|
|
|
|
@classmethod
|
|
def showWarning(cls, title: str, content: str, parent=None):
|
|
"""Convenience method for warning dialogs"""
|
|
dialog = cls(title, content, "warning", parent)
|
|
dialog.exec()
|