Files
navipy/src/widgets/accessible_text_dialog.py

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()