Skip to content

Unified Dialog System

A theme-aware dialog system providing message boxes, loading dialogs, input dialogs, and confirmation dialogs with consistent styling and behavior.

Quick Start

from src.shared_services.prompt_dialogs.api import (
    show_info,
    show_warning,
    show_error,
    ask_question,
    LoadingDialog,
    ConfirmationDialog,
    InputDialog,
)

# Simple message dialogs
show_info(parent, "Success", "File saved successfully.")
show_warning(parent, "Warning", "Unsaved changes detected.")
show_error(parent, "Error", "Failed to connect to server.")

# Question dialog
from PySide6.QtWidgets import QMessageBox

result = ask_question(parent, "Confirm", "Delete this item?")
if result == QMessageBox.StandardButton.Yes:
    delete_item()

# Loading dialog with context manager
with LoadingDialog(parent) as loading:
    loading.show_loading("Processing files...")
    for i, item in enumerate(items):
        loading.update_progress(f"Item {i + 1}/{len(items)}", int(100 * i / len(items)))
        process_item(item)

# Validated text input
name, ok = InputDialog.get_text(
    parent,
    title="New Project",
    prompt="Project name:",
    blacklist=existing_names,
)
if ok:
    create_project(name)

# Typed confirmation for destructive actions
if ConfirmationDialog.confirm(
        parent,
        item_name="important_project.xml",
        confirmation_word="Delete",
):
    delete_file("important_project.xml")

Components

MessageDialog

Theme-aware replacement for QMessageBox with static convenience methods:

  • MessageDialog.info() - Information messages
  • MessageDialog.warning() - Warning messages
  • MessageDialog.error() - Error messages
  • MessageDialog.question() - Yes/No questions

LoadingDialog

Modal loading dialog with:

  • Animated spinner
  • Progress bar with percentage
  • Subprocess status tracking
  • Message box interception (displays within dialog)
  • Fail-safe close button after timeout
  • Context manager support

InputDialog

Validated text input with:

  • Real-time validation feedback
  • Blacklist support (existing names)
  • Invalid character detection
  • Reserved name detection (CON, PRN, etc.)

ConfirmationDialog

Typed confirmation for destructive actions:

  • User must type a specific word to confirm
  • Real-time validation
  • Prevents accidental deletion

Message Interception System

During loading operations, QMessageBox calls are automatically intercepted and displayed within the loading dialog. This provides seamless user experience and prevents dialog overlap.

All interceptions are logged for developer monitoring:

from src.shared_services.prompt_dialogs.api import get_recent_interceptions

# Review recent interceptions
for entry in get_recent_interceptions(10):
    print(f"[{entry.message_type.name}] {entry.title}")
    print(f"  Source: {entry.source_file}:{entry.source_line}")

Sound Patch

Suppresses Windows system sounds for message dialogs:

from src.shared_services.prompt_dialogs.api import apply_sound_patch

# Apply at application startup
apply_sound_patch()

Styling

All components use setObjectName() for QSS targeting. Stylesheets are located in:

data/.app_data/stylesheets/dialogs/
    main.qss               # Common styles
    message_dialog.qss     # MessageDialog styles
    loading_dialog.qss     # LoadingDialog styles
    confirmation_dialog.qss # ConfirmationDialog styles
    input_dialog.qss       # InputDialog styles

Module Structure

src/shared_services/dialogs/
    __init__.py
    api.py                  # Public API exports
    README.md
    constants/
        paths.py            # PathDef definitions
    base/
        theme_aware_dialog.py  # Base class with theme support
        dialog_registry.py     # Active dialog tracking
    message_box/
        message_dialog.py   # Info, warning, error, question
    loading/
        loading_dialog.py   # Main loading dialog
        loading_spinner.py  # Spinner widget
    confirmation/
        confirmation_dialog.py  # Typed confirmation
    input/
        input_dialog.py     # Validated text input
        validators.py       # Validation logic
    interception/
        interceptor.py      # MessageBox interception
        interception_log.py # Logging for monitoring
    sound/
        sound_patch.py      # Windows sound suppression

Theme Integration

All dialogs automatically register with StylesheetManager for theme updates. When the application theme changes, dialogs update their appearance accordingly.

To create a custom theme-aware dialog:

from src.shared_services.prompt_dialogs.base.theme_aware_dialog import ThemeAwareDialog
from src.shared_services.prompt_dialogs.constants.paths import DialogStylesheets


class MyDialog(ThemeAwareDialog):
    def __init__(self, parent=None):
        super().__init__(
            parent=parent,
            stylesheets=[DialogStylesheets.Main],
        )
        self._setup_ui()

    def _on_theme_changed(self, theme: str) -> None:
        # Handle theme changes (optional)
        pass

API Reference

src.shared_services.prompt_dialogs.api

Unified Dialog System API.

Provides a clean, theme-aware API for all application dialogs including message boxes, loading dialogs, input dialogs, and confirmation dialogs.

Usage

from src.shared_services.dialogs.api import ( # Dialog classes MessageDialog, LoadingDialog, ConfirmationDialog, InputDialog,

# Convenience functions
show_info,
show_warning,
show_error,
ask_question,

)

Show a simple info message

show_info(parent, "Success", "File saved successfully.")

Show a warning with custom buttons

result = show_warning( parent, "Unsaved Changes", "You have unsaved changes. Save before closing?", buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, )

Use loading dialog with context manager

with LoadingDialog(parent) as loading: loading.show_loading("Processing files...") loading.update_progress("Step 1/3", 33) do_work()

Get validated text input

name, ok = InputDialog.get_text( parent, title="New Project", prompt="Project name:", blacklist=existing_names, ) if ok: create_project(name)

Confirm destructive action

if ConfirmationDialog.confirm( parent, item_name="important_project.xml", confirmation_word="Delete", ): delete_file("important_project.xml")

ButtonDef dataclass

Definition for a button in a MultiButtonDialog.

Attributes:

Name Type Description
text str

Button label text.

style str

Visual style -- "outlined", "filled", or "hold_to_confirm".

result str

Identifier string returned when this button is clicked.

Source code in src\shared_services\prompt_dialogs\multi_button\multi_button_dialog.py
@dataclass
class ButtonDef:
    """Definition for a button in a MultiButtonDialog.

    Attributes:
        text: Button label text.
        style: Visual style -- "outlined", "filled", or "hold_to_confirm".
        result: Identifier string returned when this button is clicked.
    """

    text: str
    style: str
    result: str

ConfirmationDialog

Bases: ThemeAwareDialog

Confirmation dialog for destructive actions.

When confirmation_word is non-empty the user must type that word before the hold-to-confirm button becomes active. When it is empty, the button is active immediately and the text input is hidden.

Signals

confirmed: Emitted when the action is confirmed.

Example

Creating a typed confirmation dialog::

dialog = ConfirmationDialog(
    parent=self,
    title="Delete Project",
    item_name="my_project.xml",
    confirmation_word="Delete",
)
if dialog.exec():
    delete_project()
Source code in src\shared_services\prompt_dialogs\confirmation\confirmation_dialog.py
class ConfirmationDialog(ThemeAwareDialog):
    """
    Confirmation dialog for destructive actions.

    When ``confirmation_word`` is non-empty the user must type that word
    before the hold-to-confirm button becomes active. When it is empty,
    the button is active immediately and the text input is hidden.

    Signals:
        confirmed: Emitted when the action is confirmed.

    Example:
        Creating a typed confirmation dialog::

            dialog = ConfirmationDialog(
                parent=self,
                title="Delete Project",
                item_name="my_project.xml",
                confirmation_word="Delete",
            )
            if dialog.exec():
                delete_project()
    """

    confirmed: Signal = Signal()

    def __init__(
        self,
        parent: QWidget | None = None,
        title: str = "Confirm Action",
        item_name: str = "",
        confirmation_word: str = "Delete",
        warning_message: str = "",
        show_confirmation_word: bool = True,
    ) -> None:
        """
        Initialize the confirmation dialog.

        Args:
            parent: Parent widget.
            title: Dialog title.
            item_name: Name of the item being deleted/modified.
            confirmation_word: Word the user must type to confirm.
                Pass empty string for simple hold-to-confirm mode.
            warning_message: Optional custom warning message.
            show_confirmation_word: If True (default), reveal the word in
                the instruction label and placeholder. If False, hide it
                (password-style confirmation).
        """
        super().__init__(
            parent=parent,
            stylesheets=[DialogStylesheets.Main, DialogStylesheets.ConfirmationDialog],
        )

        self._title = title
        self._item_name = item_name
        self._confirmation_word = confirmation_word
        self._requires_typed_confirm = bool(confirmation_word)
        self._show_confirmation_word = show_confirmation_word
        self._warning_message = warning_message or (
            "Diese Aktion ist dauerhaft und kann nicht rückgängig gemacht werden."
        )
        self._is_confirmed = False

        self._setup_ui()
        self._connect_signals()

    def _setup_ui(self) -> None:
        """Set up the dialog UI."""
        self.setObjectName("ConfirmationDialog")
        self.setWindowTitle(self._title)
        self.setMinimumWidth(480 + 2 * self._SHADOW_MARGIN)
        self.setMaximumWidth(560 + 2 * self._SHADOW_MARGIN)

        from PySide6.QtGui import QColor
        self._accent_color = QColor(220, 38, 38)

        # Main dialog layout (transparent)
        dialog_layout = QVBoxLayout(self)
        dialog_layout.setContentsMargins(
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
        )

        # Container frame
        self._container = QFrame()
        self._container.setObjectName("ConfirmationDialog_Container")

        # Container layout - compact
        container_layout = QVBoxLayout(self._container)
        container_layout.setSpacing(8)
        container_layout.setContentsMargins(14, 12, 14, 12)

        # Header with icon
        header_layout = QHBoxLayout()
        header_layout.setSpacing(10)

        # Warning icon
        self._icon_label = QLabel()
        self._icon_label.setObjectName("ConfirmationDialog_Icon")
        self._icon_label.setFixedSize(24, 24)
        self._setup_icon()
        header_layout.addWidget(self._icon_label, 0, Qt.AlignmentFlag.AlignTop)

        # Header text area
        header_text_layout = QVBoxLayout()
        header_text_layout.setSpacing(2)

        # Title in header
        title_label = QLabel(self._title)
        title_label.setObjectName("ConfirmationDialog_Title")
        header_text_layout.addWidget(title_label)

        # Item name (if provided)
        if self._item_name:
            item_label = QLabel(f"<b>{self._item_name}</b>")
            item_label.setObjectName("ConfirmationDialog_ItemLabel")
            item_label.setWordWrap(True)
            header_text_layout.addWidget(item_label)

        header_layout.addLayout(header_text_layout, 1)
        container_layout.addLayout(header_layout)

        # Warning message
        warning_label = QLabel(self._warning_message)
        warning_label.setObjectName("ConfirmationDialog_WarningLabel")
        warning_label.setWordWrap(True)
        container_layout.addWidget(warning_label)

        # Typed-confirmation widgets (hidden in simple mode)
        if self._show_confirmation_word:
            instruction_text = (
                f'"<b>{self._confirmation_word}</b>" eingeben zum Bestätigen:'
            )
            placeholder = self._confirmation_word
        else:
            instruction_text = "Löschpasswort eingeben:"
            placeholder = ""

        self._instruction_label = QLabel(instruction_text)
        self._instruction_label.setObjectName("ConfirmationDialog_InstructionLabel")
        container_layout.addWidget(self._instruction_label)

        self._confirmation_input = QLineEdit()
        self._confirmation_input.setObjectName("ConfirmationDialog_Input")
        self._confirmation_input.setPlaceholderText(placeholder)
        container_layout.addWidget(self._confirmation_input)

        self._status_label = QLabel()
        self._status_label.setObjectName("ConfirmationDialog_StatusLabel")
        self._status_label.hide()
        container_layout.addWidget(self._status_label)

        # Hide typed-confirmation widgets in simple mode
        if not self._requires_typed_confirm:
            self._instruction_label.hide()
            self._confirmation_input.hide()

        # Button area
        button_layout = QHBoxLayout()
        button_layout.setSpacing(6)
        button_layout.addStretch()

        # Cancel button
        self._cancel_button = QPushButton("Abbrechen")
        self._cancel_button.setObjectName("ConfirmationDialog_CancelButton")
        button_layout.addWidget(self._cancel_button)

        # Hold-to-confirm button
        self._confirm_button = HoldToConfirmButton("Bestätigen")
        self._confirm_button.setObjectName("ConfirmationDialog_ConfirmButton")

        if self._requires_typed_confirm:
            # Lock until the correct word is typed
            self._confirm_button.set_armed_enabled(False)
        else:
            # Simple mode: button is immediately hover-ready
            self._confirm_button.set_armed_enabled(True)

        button_layout.addWidget(self._confirm_button)

        container_layout.addLayout(button_layout)

        dialog_layout.addWidget(self._container)

        # Focus on input in typed mode, otherwise on cancel
        if self._requires_typed_confirm:
            self._confirmation_input.setFocus()
        else:
            self._cancel_button.setFocus()

    def _setup_icon(self) -> None:
        """Set up the warning icon."""
        try:
            from src.shared_services.rendering.icons.icon_paths import Icons
            from src.shared_services.rendering.icons.api import render_svg

            self._icon_label.setPixmap(render_svg(Icons.Alert.Warning, size=24))
        except (ImportError, FileNotFoundError):
            # Fall back to standard icon
            style = self.style()
            pixmap = style.standardPixmap(style.StandardPixmap.SP_MessageBoxWarning)
            scaled = pixmap.scaled(
                24, 24,
                Qt.AspectRatioMode.KeepAspectRatio,
                Qt.TransformationMode.SmoothTransformation,
            )
            self._icon_label.setPixmap(scaled)

    def _connect_signals(self) -> None:
        """Connect signals to slots."""
        if self._requires_typed_confirm:
            self._confirmation_input.textChanged.connect(self._validate_confirmation)
            self._confirmation_input.returnPressed.connect(self._try_confirm)
        self._confirm_button.confirmed.connect(self._do_confirm)
        self._cancel_button.clicked.connect(self.reject)

    @Slot(str)
    def _validate_confirmation(self, text: str) -> None:
        """
        Validate the confirmation input.

        Args:
            text: Current input text.
        """
        if text == self._confirmation_word:
            self._set_valid_state()
        else:
            self._set_invalid_state(text)

    def _set_valid_state(self) -> None:
        """Set UI to valid (confirmed) state."""
        self._status_label.setText("Bestätigung korrekt")
        self._status_label.setProperty("validationState", "valid")
        self.refresh_style(self._status_label)
        self._status_label.show()

        self._confirm_button.set_armed_enabled(True)

    def _set_invalid_state(self, text: str) -> None:
        """
        Set UI to invalid state.

        Args:
            text: Current input text.
        """
        if text:
            if self._show_confirmation_word:
                hint = f'Bitte genau "{self._confirmation_word}" eingeben'
            else:
                hint = "Passwort ist nicht korrekt"
            self._status_label.setText(hint)
            self._status_label.setProperty("validationState", "hint")
            self.refresh_style(self._status_label)
            self._status_label.show()
        else:
            self._status_label.hide()

        self._confirm_button.set_armed_enabled(False)

    @Slot(str)
    def _on_theme_changed(self, theme: str) -> None:
        """Update icon when theme changes."""
        self._setup_icon()
        if self._requires_typed_confirm:
            if self._confirmation_input.text() == self._confirmation_word:
                self._set_valid_state()
            else:
                self._set_invalid_state(self._confirmation_input.text())

    @Slot()
    def _try_confirm(self) -> None:
        """Try to confirm (Enter key pressed)."""
        if self._confirm_button.is_armed():
            self._do_confirm()

    @Slot()
    def _do_confirm(self) -> None:
        """Confirm the action and close dialog."""
        self._is_confirmed = True
        self.confirmed.emit()
        self.accept()

    def is_confirmed(self) -> bool:
        """
        Check if the action was confirmed.

        Returns:
            True if the user confirmed the action.
        """
        return self._is_confirmed

    # =========================================================================
    # STATIC CONVENIENCE METHOD
    # =========================================================================

    @staticmethod
    def confirm(
        parent: QWidget | None = None,
        title: str = "Confirm Delete",
        item_name: str = "",
        confirmation_word: str = "Delete",
        warning_message: str = "",
        show_confirmation_word: bool = True,
    ) -> bool:
        """
        Show confirmation dialog and return result.

        Args:
            parent: Parent widget.
            title: Dialog title.
            item_name: Name of item being deleted.
            confirmation_word: Word user must type to confirm.
                Pass empty string for simple hold-to-confirm mode.
            warning_message: Optional custom warning message.
            show_confirmation_word: If True (default), reveal the word.
                If False, hide it (password-style confirmation).

        Returns:
            True if the user confirmed, False otherwise.

        Example:
            Typed confirmation::

                if ConfirmationDialog.confirm(
                    self,
                    item_name="project.xml",
                ):
                    delete_file("project.xml")

            Simple hold-to-confirm::

                if ConfirmationDialog.confirm(
                    self,
                    title="Element loeschen",
                    confirmation_word="",
                    warning_message="Wirklich loeschen?",
                ):
                    delete_element()
        """
        dialog = ConfirmationDialog(
            parent=parent,
            title=title,
            item_name=item_name,
            confirmation_word=confirmation_word,
            warning_message=warning_message,
            show_confirmation_word=show_confirmation_word,
        )

        result = dialog.exec()
        return result == ConfirmationDialog.DialogCode.Accepted and dialog.is_confirmed()
__init__(parent=None, title='Confirm Action', item_name='', confirmation_word='Delete', warning_message='', show_confirmation_word=True)

Initialize the confirmation dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

None
title str

Dialog title.

'Confirm Action'
item_name str

Name of the item being deleted/modified.

''
confirmation_word str

Word the user must type to confirm. Pass empty string for simple hold-to-confirm mode.

'Delete'
warning_message str

Optional custom warning message.

''
show_confirmation_word bool

If True (default), reveal the word in the instruction label and placeholder. If False, hide it (password-style confirmation).

True
Source code in src\shared_services\prompt_dialogs\confirmation\confirmation_dialog.py
def __init__(
    self,
    parent: QWidget | None = None,
    title: str = "Confirm Action",
    item_name: str = "",
    confirmation_word: str = "Delete",
    warning_message: str = "",
    show_confirmation_word: bool = True,
) -> None:
    """
    Initialize the confirmation dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        item_name: Name of the item being deleted/modified.
        confirmation_word: Word the user must type to confirm.
            Pass empty string for simple hold-to-confirm mode.
        warning_message: Optional custom warning message.
        show_confirmation_word: If True (default), reveal the word in
            the instruction label and placeholder. If False, hide it
            (password-style confirmation).
    """
    super().__init__(
        parent=parent,
        stylesheets=[DialogStylesheets.Main, DialogStylesheets.ConfirmationDialog],
    )

    self._title = title
    self._item_name = item_name
    self._confirmation_word = confirmation_word
    self._requires_typed_confirm = bool(confirmation_word)
    self._show_confirmation_word = show_confirmation_word
    self._warning_message = warning_message or (
        "Diese Aktion ist dauerhaft und kann nicht rückgängig gemacht werden."
    )
    self._is_confirmed = False

    self._setup_ui()
    self._connect_signals()
confirm(parent=None, title='Confirm Delete', item_name='', confirmation_word='Delete', warning_message='', show_confirmation_word=True) staticmethod

Show confirmation dialog and return result.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

None
title str

Dialog title.

'Confirm Delete'
item_name str

Name of item being deleted.

''
confirmation_word str

Word user must type to confirm. Pass empty string for simple hold-to-confirm mode.

'Delete'
warning_message str

Optional custom warning message.

''
show_confirmation_word bool

If True (default), reveal the word. If False, hide it (password-style confirmation).

True

Returns:

Type Description
bool

True if the user confirmed, False otherwise.

Example

Typed confirmation::

if ConfirmationDialog.confirm(
    self,
    item_name="project.xml",
):
    delete_file("project.xml")

Simple hold-to-confirm::

if ConfirmationDialog.confirm(
    self,
    title="Element loeschen",
    confirmation_word="",
    warning_message="Wirklich loeschen?",
):
    delete_element()
Source code in src\shared_services\prompt_dialogs\confirmation\confirmation_dialog.py
@staticmethod
def confirm(
    parent: QWidget | None = None,
    title: str = "Confirm Delete",
    item_name: str = "",
    confirmation_word: str = "Delete",
    warning_message: str = "",
    show_confirmation_word: bool = True,
) -> bool:
    """
    Show confirmation dialog and return result.

    Args:
        parent: Parent widget.
        title: Dialog title.
        item_name: Name of item being deleted.
        confirmation_word: Word user must type to confirm.
            Pass empty string for simple hold-to-confirm mode.
        warning_message: Optional custom warning message.
        show_confirmation_word: If True (default), reveal the word.
            If False, hide it (password-style confirmation).

    Returns:
        True if the user confirmed, False otherwise.

    Example:
        Typed confirmation::

            if ConfirmationDialog.confirm(
                self,
                item_name="project.xml",
            ):
                delete_file("project.xml")

        Simple hold-to-confirm::

            if ConfirmationDialog.confirm(
                self,
                title="Element loeschen",
                confirmation_word="",
                warning_message="Wirklich loeschen?",
            ):
                delete_element()
    """
    dialog = ConfirmationDialog(
        parent=parent,
        title=title,
        item_name=item_name,
        confirmation_word=confirmation_word,
        warning_message=warning_message,
        show_confirmation_word=show_confirmation_word,
    )

    result = dialog.exec()
    return result == ConfirmationDialog.DialogCode.Accepted and dialog.is_confirmed()
is_confirmed()

Check if the action was confirmed.

Returns:

Type Description
bool

True if the user confirmed the action.

Source code in src\shared_services\prompt_dialogs\confirmation\confirmation_dialog.py
def is_confirmed(self) -> bool:
    """
    Check if the action was confirmed.

    Returns:
        True if the user confirmed the action.
    """
    return self._is_confirmed

DialogRegistry

Singleton registry for tracking active dialogs.

Uses weak references to automatically clean up deleted dialogs. Provides methods for querying active dialogs by type.

The registry is primarily used by the interception system to determine if a loading dialog is active and should intercept message box calls.

Thread-safe singleton implementation using a lock.

Example

Registering a dialog::

registry = DialogRegistry.instance()
registry.register(loading_dialog, "loading")

Checking for active loading::

if registry.is_loading_active():
    loading = registry.get_active_loading_dialog()
Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
class DialogRegistry:
    """
    Singleton registry for tracking active dialogs.

    Uses weak references to automatically clean up deleted dialogs.
    Provides methods for querying active dialogs by type.

    The registry is primarily used by the interception system to
    determine if a loading dialog is active and should intercept
    message box calls.

    Thread-safe singleton implementation using a lock.

    Example:
        Registering a dialog::

            registry = DialogRegistry.instance()
            registry.register(loading_dialog, "loading")

        Checking for active loading::

            if registry.is_loading_active():
                loading = registry.get_active_loading_dialog()
    """

    _instance: Optional["DialogRegistry"] = None
    _lock: threading.Lock = threading.Lock()

    def __init__(self) -> None:
        """Initialize the dialog registry."""
        self._registered: List[RegisteredDialog] = []

    @classmethod
    def instance(cls) -> "DialogRegistry":
        """
        Get the singleton instance.

        Thread-safe lazy initialization.

        Returns:
            The shared DialogRegistry instance.
        """
        if cls._instance is None:
            with cls._lock:
                # Double-check locking pattern
                if cls._instance is None:
                    cls._instance = cls()
        return cls._instance

    @classmethod
    def reset_instance(cls) -> None:
        """
        Reset the singleton instance.

        Useful for testing.
        """
        with cls._lock:
            cls._instance = None

    def register(self, dialog: "ThemeAwareDialog", dialog_type: str) -> None:
        """
        Register a dialog for tracking.

        Args:
            dialog: The dialog to track.
            dialog_type: Type identifier (e.g., "loading", "message").
        """
        # Clean up dead references first
        self._cleanup()

        # Check if already registered
        for entry in self._registered:
            if entry.dialog_ref() is dialog:
                return

        # Create weak reference with cleanup callback
        def on_delete(ref: weakref.ref) -> None:
            self._remove_by_ref(ref)

        dialog_ref = weakref.ref(dialog, on_delete)

        entry = RegisteredDialog(
            dialog_ref=dialog_ref,
            dialog_type=dialog_type,
        )

        self._registered.append(entry)

    def unregister(self, dialog: "ThemeAwareDialog") -> bool:
        """
        Unregister a dialog.

        Args:
            dialog: The dialog to unregister.

        Returns:
            True if the dialog was found and unregistered.
        """
        initial_count = len(self._registered)
        self._registered = [
            entry
            for entry in self._registered
            if entry.dialog_ref() is not dialog
        ]
        return len(self._registered) < initial_count

    def is_loading_active(self) -> bool:
        """
        Check if any loading dialog is currently active.

        Returns:
            True if at least one loading dialog is registered and visible.
        """
        self._cleanup()

        for entry in self._registered:
            if entry.dialog_type == "loading":
                dialog = entry.dialog_ref()
                if dialog is not None and self._is_dialog_visible(dialog):
                    return True

        return False

    def get_active_loading_dialog(self) -> Optional["ThemeAwareDialog"]:
        """
        Get the currently active loading dialog.

        Returns:
            The active loading dialog, or None if none is active.
        """
        self._cleanup()

        for entry in self._registered:
            if entry.dialog_type == "loading":
                dialog = entry.dialog_ref()
                if dialog is not None and self._is_dialog_visible(dialog):
                    return dialog

        return None

    def get_dialogs_by_type(self, dialog_type: str) -> List["ThemeAwareDialog"]:
        """
        Get all registered dialogs of a specific type.

        Args:
            dialog_type: Type identifier to filter by.

        Returns:
            List of dialogs matching the type.
        """
        self._cleanup()

        dialogs = []
        for entry in self._registered:
            if entry.dialog_type == dialog_type:
                dialog = entry.dialog_ref()
                if dialog is not None:
                    dialogs.append(dialog)

        return dialogs

    def get_registered_count(self) -> int:
        """
        Get the number of registered dialogs.

        Returns:
            Count of registered dialogs.
        """
        self._cleanup()
        return len(self._registered)

    def _cleanup(self) -> None:
        """Remove references to deleted dialogs."""
        self._registered = [
            entry
            for entry in self._registered
            if entry.dialog_ref() is not None
        ]

    def _remove_by_ref(self, ref: weakref.ref) -> None:
        """Remove entry by weak reference."""
        self._registered = [
            entry
            for entry in self._registered
            if entry.dialog_ref is not ref
        ]

    def _is_dialog_visible(self, dialog: "ThemeAwareDialog") -> bool:
        """
        Check if a dialog is visible.

        Args:
            dialog: The dialog to check.

        Returns:
            True if the dialog is visible.
        """
        try:
            return dialog.isVisible()
        except RuntimeError:
            # Dialog may have been deleted
            return False
__init__()

Initialize the dialog registry.

Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
def __init__(self) -> None:
    """Initialize the dialog registry."""
    self._registered: List[RegisteredDialog] = []
get_active_loading_dialog()

Get the currently active loading dialog.

Returns:

Type Description
Optional[ThemeAwareDialog]

The active loading dialog, or None if none is active.

Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
def get_active_loading_dialog(self) -> Optional["ThemeAwareDialog"]:
    """
    Get the currently active loading dialog.

    Returns:
        The active loading dialog, or None if none is active.
    """
    self._cleanup()

    for entry in self._registered:
        if entry.dialog_type == "loading":
            dialog = entry.dialog_ref()
            if dialog is not None and self._is_dialog_visible(dialog):
                return dialog

    return None
get_dialogs_by_type(dialog_type)

Get all registered dialogs of a specific type.

Parameters:

Name Type Description Default
dialog_type str

Type identifier to filter by.

required

Returns:

Type Description
List[ThemeAwareDialog]

List of dialogs matching the type.

Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
def get_dialogs_by_type(self, dialog_type: str) -> List["ThemeAwareDialog"]:
    """
    Get all registered dialogs of a specific type.

    Args:
        dialog_type: Type identifier to filter by.

    Returns:
        List of dialogs matching the type.
    """
    self._cleanup()

    dialogs = []
    for entry in self._registered:
        if entry.dialog_type == dialog_type:
            dialog = entry.dialog_ref()
            if dialog is not None:
                dialogs.append(dialog)

    return dialogs
get_registered_count()

Get the number of registered dialogs.

Returns:

Type Description
int

Count of registered dialogs.

Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
def get_registered_count(self) -> int:
    """
    Get the number of registered dialogs.

    Returns:
        Count of registered dialogs.
    """
    self._cleanup()
    return len(self._registered)
instance() classmethod

Get the singleton instance.

Thread-safe lazy initialization.

Returns:

Type Description
DialogRegistry

The shared DialogRegistry instance.

Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
@classmethod
def instance(cls) -> "DialogRegistry":
    """
    Get the singleton instance.

    Thread-safe lazy initialization.

    Returns:
        The shared DialogRegistry instance.
    """
    if cls._instance is None:
        with cls._lock:
            # Double-check locking pattern
            if cls._instance is None:
                cls._instance = cls()
    return cls._instance
is_loading_active()

Check if any loading dialog is currently active.

Returns:

Type Description
bool

True if at least one loading dialog is registered and visible.

Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
def is_loading_active(self) -> bool:
    """
    Check if any loading dialog is currently active.

    Returns:
        True if at least one loading dialog is registered and visible.
    """
    self._cleanup()

    for entry in self._registered:
        if entry.dialog_type == "loading":
            dialog = entry.dialog_ref()
            if dialog is not None and self._is_dialog_visible(dialog):
                return True

    return False
register(dialog, dialog_type)

Register a dialog for tracking.

Parameters:

Name Type Description Default
dialog ThemeAwareDialog

The dialog to track.

required
dialog_type str

Type identifier (e.g., "loading", "message").

required
Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
def register(self, dialog: "ThemeAwareDialog", dialog_type: str) -> None:
    """
    Register a dialog for tracking.

    Args:
        dialog: The dialog to track.
        dialog_type: Type identifier (e.g., "loading", "message").
    """
    # Clean up dead references first
    self._cleanup()

    # Check if already registered
    for entry in self._registered:
        if entry.dialog_ref() is dialog:
            return

    # Create weak reference with cleanup callback
    def on_delete(ref: weakref.ref) -> None:
        self._remove_by_ref(ref)

    dialog_ref = weakref.ref(dialog, on_delete)

    entry = RegisteredDialog(
        dialog_ref=dialog_ref,
        dialog_type=dialog_type,
    )

    self._registered.append(entry)
reset_instance() classmethod

Reset the singleton instance.

Useful for testing.

Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
@classmethod
def reset_instance(cls) -> None:
    """
    Reset the singleton instance.

    Useful for testing.
    """
    with cls._lock:
        cls._instance = None
unregister(dialog)

Unregister a dialog.

Parameters:

Name Type Description Default
dialog ThemeAwareDialog

The dialog to unregister.

required

Returns:

Type Description
bool

True if the dialog was found and unregistered.

Source code in src\shared_services\prompt_dialogs\base\dialog_registry.py
def unregister(self, dialog: "ThemeAwareDialog") -> bool:
    """
    Unregister a dialog.

    Args:
        dialog: The dialog to unregister.

    Returns:
        True if the dialog was found and unregistered.
    """
    initial_count = len(self._registered)
    self._registered = [
        entry
        for entry in self._registered
        if entry.dialog_ref() is not dialog
    ]
    return len(self._registered) < initial_count

DialogStyle

Bases: Enum

Visual style presets for MultiButtonDialog.

Controls the icon and accent color without changing functionality.

Source code in src\shared_services\prompt_dialogs\multi_button\multi_button_dialog.py
class DialogStyle(Enum):
    """Visual style presets for MultiButtonDialog.

    Controls the icon and accent color without changing functionality.
    """

    INFO = auto()
    WARNING = auto()
    ERROR = auto()

InputDialog

Bases: ThemeAwareDialog

Dialog for validated text input.

Provides real-time validation with error feedback. Supports blacklists, invalid character detection, and reserved name checking.

Signals

valid_input_entered: Emitted when valid input is accepted.

Example

Using the dialog::

dialog = InputDialog(
    parent=self,
    title="Rename File",
    prompt="New name:",
    blacklist=existing_files,
    initial_text="document",
)
if dialog.exec():
    new_name = dialog.get_entered_text()
Source code in src\shared_services\prompt_dialogs\input\input_dialog.py
class InputDialog(ThemeAwareDialog):
    """
    Dialog for validated text input.

    Provides real-time validation with error feedback. Supports
    blacklists, invalid character detection, and reserved name checking.

    Signals:
        valid_input_entered: Emitted when valid input is accepted.

    Example:
        Using the dialog::

            dialog = InputDialog(
                parent=self,
                title="Rename File",
                prompt="New name:",
                blacklist=existing_files,
                initial_text="document",
            )
            if dialog.exec():
                new_name = dialog.get_entered_text()
    """

    valid_input_entered: Signal = Signal(str)

    def __init__(
        self,
        parent: QWidget | None = None,
        title: str = "Enter Name",
        prompt: str = "Name:",
        blacklist: List[str] | None = None,
        allow_empty: bool = False,
        initial_text: str = "",
        placeholder_text: str = "Enter a valid name...",
        valid_text: str = "Valid name",
        error_text: str = "Invalid input.",
    ) -> None:
        """
        Initialize the input dialog.

        Args:
            parent: Parent widget.
            title: Dialog title.
            prompt: Label text for the input field.
            blacklist: List of names that are not allowed.
            allow_empty: Whether empty input is allowed.
            initial_text: Initial text in the input field.
            placeholder_text: Placeholder text shown in the input field.
            valid_text: Text shown when input is valid.
            error_text: Fallback text shown when input is invalid.
        """
        super().__init__(
            parent=parent,
            stylesheets=[DialogStylesheets.Main, DialogStylesheets.InputDialog],
        )

        self._title = title
        self._prompt = prompt
        self._initial_text = initial_text
        self._placeholder_text = placeholder_text
        self._valid_text = valid_text
        self._error_text = error_text
        self._result_text = ""

        # Create validator
        self._validator = InputValidator(
            blacklist=blacklist,
            allow_empty=allow_empty,
        )

        self._setup_ui()
        self._connect_signals()

        # Set initial text and validate
        if initial_text:
            self._input_field.setText(initial_text)
        self._validate_input(self._input_field.text())

    def _setup_ui(self) -> None:
        """Set up the dialog UI."""
        self.setObjectName("InputDialog")
        self.setWindowTitle(self._title)
        self.setMinimumWidth(450 + 2 * self._SHADOW_MARGIN)
        self.setMaximumWidth(520 + 2 * self._SHADOW_MARGIN)

        from PySide6.QtGui import QColor
        self._accent_color = QColor(37, 99, 235)

        # Main dialog layout (transparent)
        dialog_layout = QVBoxLayout(self)
        dialog_layout.setContentsMargins(
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
        )

        # Container frame
        self._container = QFrame()
        self._container.setObjectName("InputDialog_Container")

        # Container layout - compact
        container_layout = QVBoxLayout(self._container)
        container_layout.setSpacing(8)
        container_layout.setContentsMargins(14, 12, 14, 12)

        # Prompt label
        self._prompt_label = QLabel(self._prompt)
        self._prompt_label.setObjectName("InputDialog_PromptLabel")
        container_layout.addWidget(self._prompt_label)

        # Input field
        self._input_field = QLineEdit()
        self._input_field.setObjectName("InputDialog_Input")
        self._input_field.setPlaceholderText(self._placeholder_text)
        container_layout.addWidget(self._input_field)

        # Error label
        self._error_label = QLabel()
        self._error_label.setObjectName("InputDialog_ErrorLabel")
        self._error_label.setWordWrap(True)
        self._error_label.hide()
        container_layout.addWidget(self._error_label)

        # Status label (shows valid indicator)
        self._status_label = QLabel()
        self._status_label.setObjectName("InputDialog_StatusLabel")
        self._status_label.hide()
        container_layout.addWidget(self._status_label)

        # Button area
        button_layout = QHBoxLayout()
        button_layout.setSpacing(6)
        button_layout.addStretch()

        # Cancel button
        self._cancel_button = QPushButton("Cancel")
        self._cancel_button.setObjectName("InputDialog_CancelButton")
        button_layout.addWidget(self._cancel_button)

        # OK button
        self._ok_button = QPushButton("OK")
        self._ok_button.setObjectName("InputDialog_OkButton")
        self._ok_button.setDefault(True)
        self._ok_button.setEnabled(False)
        button_layout.addWidget(self._ok_button)

        container_layout.addLayout(button_layout)

        dialog_layout.addWidget(self._container)

        # Focus on input
        self._input_field.setFocus()

    @Slot(str)
    def _on_theme_changed(self, theme: str) -> None:
        """Handle theme changes - styles are applied via QSS."""
        pass

    def _connect_signals(self) -> None:
        """Connect signals to slots."""
        self._input_field.textChanged.connect(self._validate_input)
        self._input_field.returnPressed.connect(self._try_accept)
        self._ok_button.clicked.connect(self._accept_input)
        self._cancel_button.clicked.connect(self.reject)

    @Slot(str)
    def _validate_input(self, text: str) -> None:
        """
        Validate input in real-time.

        Args:
            text: Current input text.
        """
        result = self._validator.validate(text)

        if result.is_valid:
            self._set_valid_state()
        else:
            self._set_error_state(result.error_message or self._error_text)

    def _set_valid_state(self) -> None:
        """Set UI to valid state."""
        self._error_label.hide()
        self._status_label.setText(self._valid_text)
        self._status_label.setProperty("validationState", "valid")
        self.refresh_style(self._status_label)
        self._status_label.show()
        self._ok_button.setEnabled(True)

    def _set_error_state(self, message: str) -> None:
        """
        Set UI to error state.

        Args:
            message: Error message to display.
        """
        self._error_label.setText(message)
        self._error_label.setProperty("validationState", "error")
        self.refresh_style(self._error_label)
        self._error_label.show()
        self._status_label.hide()
        self._ok_button.setEnabled(False)

    @Slot()
    def _try_accept(self) -> None:
        """Try to accept input (Enter key pressed)."""
        if self._ok_button.isEnabled():
            self._accept_input()

    @Slot()
    def _accept_input(self) -> None:
        """Accept the input and close dialog."""
        text = self._input_field.text().strip()
        if self._ok_button.isEnabled():
            self._result_text = text
            self.valid_input_entered.emit(text)
            self.accept()

    def get_entered_text(self) -> str:
        """
        Get the entered text after dialog closes.

        Returns:
            The validated text entered by the user.
        """
        return self._result_text

    def set_text(self, text: str) -> None:
        """
        Set the input field text.

        Args:
            text: Text to set.
        """
        self._input_field.setText(text)

    # =========================================================================
    # STATIC CONVENIENCE METHOD
    # =========================================================================

    @staticmethod
    def get_text(
        parent: QWidget | None = None,
        title: str = "Enter Name",
        prompt: str = "Name:",
        blacklist: List[str] | None = None,
        allow_empty: bool = False,
        initial_text: str = "",
        placeholder_text: str = "Enter a valid name...",
        valid_text: str = "Valid name",
        error_text: str = "Invalid input.",
    ) -> Tuple[str, bool]:
        """
        Show input dialog and get validated text.

        Args:
            parent: Parent widget.
            title: Dialog title.
            prompt: Label text for input field.
            blacklist: List of disallowed names.
            allow_empty: Whether empty input is allowed.
            initial_text: Initial text in the input field.
            placeholder_text: Placeholder text shown in the input field.
            valid_text: Text shown when input is valid.
            error_text: Fallback text shown when input is invalid.

        Returns:
            Tuple of (text, accepted). Text is empty if not accepted.

        Example:
            Getting a project name::

                name, ok = InputDialog.get_text(
                    self,
                    title="New Project",
                    prompt="Project name:",
                    blacklist=existing_projects,
                )
                if ok:
                    create_project(name)
        """
        dialog = InputDialog(
            parent=parent,
            title=title,
            prompt=prompt,
            blacklist=blacklist,
            allow_empty=allow_empty,
            initial_text=initial_text,
            placeholder_text=placeholder_text,
            valid_text=valid_text,
            error_text=error_text,
        )

        result = dialog.exec()
        accepted = result == InputDialog.DialogCode.Accepted

        return dialog.get_entered_text() if accepted else "", accepted
__init__(parent=None, title='Enter Name', prompt='Name:', blacklist=None, allow_empty=False, initial_text='', placeholder_text='Enter a valid name...', valid_text='Valid name', error_text='Invalid input.')

Initialize the input dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

None
title str

Dialog title.

'Enter Name'
prompt str

Label text for the input field.

'Name:'
blacklist List[str] | None

List of names that are not allowed.

None
allow_empty bool

Whether empty input is allowed.

False
initial_text str

Initial text in the input field.

''
placeholder_text str

Placeholder text shown in the input field.

'Enter a valid name...'
valid_text str

Text shown when input is valid.

'Valid name'
error_text str

Fallback text shown when input is invalid.

'Invalid input.'
Source code in src\shared_services\prompt_dialogs\input\input_dialog.py
def __init__(
    self,
    parent: QWidget | None = None,
    title: str = "Enter Name",
    prompt: str = "Name:",
    blacklist: List[str] | None = None,
    allow_empty: bool = False,
    initial_text: str = "",
    placeholder_text: str = "Enter a valid name...",
    valid_text: str = "Valid name",
    error_text: str = "Invalid input.",
) -> None:
    """
    Initialize the input dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        prompt: Label text for the input field.
        blacklist: List of names that are not allowed.
        allow_empty: Whether empty input is allowed.
        initial_text: Initial text in the input field.
        placeholder_text: Placeholder text shown in the input field.
        valid_text: Text shown when input is valid.
        error_text: Fallback text shown when input is invalid.
    """
    super().__init__(
        parent=parent,
        stylesheets=[DialogStylesheets.Main, DialogStylesheets.InputDialog],
    )

    self._title = title
    self._prompt = prompt
    self._initial_text = initial_text
    self._placeholder_text = placeholder_text
    self._valid_text = valid_text
    self._error_text = error_text
    self._result_text = ""

    # Create validator
    self._validator = InputValidator(
        blacklist=blacklist,
        allow_empty=allow_empty,
    )

    self._setup_ui()
    self._connect_signals()

    # Set initial text and validate
    if initial_text:
        self._input_field.setText(initial_text)
    self._validate_input(self._input_field.text())
get_entered_text()

Get the entered text after dialog closes.

Returns:

Type Description
str

The validated text entered by the user.

Source code in src\shared_services\prompt_dialogs\input\input_dialog.py
def get_entered_text(self) -> str:
    """
    Get the entered text after dialog closes.

    Returns:
        The validated text entered by the user.
    """
    return self._result_text
get_text(parent=None, title='Enter Name', prompt='Name:', blacklist=None, allow_empty=False, initial_text='', placeholder_text='Enter a valid name...', valid_text='Valid name', error_text='Invalid input.') staticmethod

Show input dialog and get validated text.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

None
title str

Dialog title.

'Enter Name'
prompt str

Label text for input field.

'Name:'
blacklist List[str] | None

List of disallowed names.

None
allow_empty bool

Whether empty input is allowed.

False
initial_text str

Initial text in the input field.

''
placeholder_text str

Placeholder text shown in the input field.

'Enter a valid name...'
valid_text str

Text shown when input is valid.

'Valid name'
error_text str

Fallback text shown when input is invalid.

'Invalid input.'

Returns:

Type Description
Tuple[str, bool]

Tuple of (text, accepted). Text is empty if not accepted.

Example

Getting a project name::

name, ok = InputDialog.get_text(
    self,
    title="New Project",
    prompt="Project name:",
    blacklist=existing_projects,
)
if ok:
    create_project(name)
Source code in src\shared_services\prompt_dialogs\input\input_dialog.py
@staticmethod
def get_text(
    parent: QWidget | None = None,
    title: str = "Enter Name",
    prompt: str = "Name:",
    blacklist: List[str] | None = None,
    allow_empty: bool = False,
    initial_text: str = "",
    placeholder_text: str = "Enter a valid name...",
    valid_text: str = "Valid name",
    error_text: str = "Invalid input.",
) -> Tuple[str, bool]:
    """
    Show input dialog and get validated text.

    Args:
        parent: Parent widget.
        title: Dialog title.
        prompt: Label text for input field.
        blacklist: List of disallowed names.
        allow_empty: Whether empty input is allowed.
        initial_text: Initial text in the input field.
        placeholder_text: Placeholder text shown in the input field.
        valid_text: Text shown when input is valid.
        error_text: Fallback text shown when input is invalid.

    Returns:
        Tuple of (text, accepted). Text is empty if not accepted.

    Example:
        Getting a project name::

            name, ok = InputDialog.get_text(
                self,
                title="New Project",
                prompt="Project name:",
                blacklist=existing_projects,
            )
            if ok:
                create_project(name)
    """
    dialog = InputDialog(
        parent=parent,
        title=title,
        prompt=prompt,
        blacklist=blacklist,
        allow_empty=allow_empty,
        initial_text=initial_text,
        placeholder_text=placeholder_text,
        valid_text=valid_text,
        error_text=error_text,
    )

    result = dialog.exec()
    accepted = result == InputDialog.DialogCode.Accepted

    return dialog.get_entered_text() if accepted else "", accepted
set_text(text)

Set the input field text.

Parameters:

Name Type Description Default
text str

Text to set.

required
Source code in src\shared_services\prompt_dialogs\input\input_dialog.py
def set_text(self, text: str) -> None:
    """
    Set the input field text.

    Args:
        text: Text to set.
    """
    self._input_field.setText(text)

InputValidator

Validator for text input.

Checks for: - Empty input (if not allowed) - Invalid file name characters - Reserved system names (Windows) - Names ending with period - Blacklisted names (case-insensitive)

Example

Basic validation::

validator = InputValidator(
    blacklist=["existing_project"],
    allow_empty=False,
)
result = validator.validate("new_project")
if not result.is_valid:
    print(result.error_message)
Source code in src\shared_services\prompt_dialogs\input\validators.py
class InputValidator:
    """
    Validator for text input.

    Checks for:
    - Empty input (if not allowed)
    - Invalid file name characters
    - Reserved system names (Windows)
    - Names ending with period
    - Blacklisted names (case-insensitive)

    Example:
        Basic validation::

            validator = InputValidator(
                blacklist=["existing_project"],
                allow_empty=False,
            )
            result = validator.validate("new_project")
            if not result.is_valid:
                print(result.error_message)
    """

    # Characters invalid in file names (Windows, macOS, Linux)
    INVALID_CHARS_PATTERN: Final[str] = r'[<>:"/\\|?*\x00-\x1f]'

    # Reserved names (Windows)
    RESERVED_NAMES: Final[Set[str]] = {
        "CON", "PRN", "AUX", "NUL",
        "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
        "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
    }

    def __init__(
        self,
        blacklist: List[str] | None = None,
        allow_empty: bool = False,
        max_length: int = 255,
        custom_pattern: str | None = None,
        custom_error: str | None = None,
    ) -> None:
        """
        Initialize the validator.

        Args:
            blacklist: List of names that are not allowed.
            allow_empty: Whether empty input is allowed.
            max_length: Maximum allowed length.
            custom_pattern: Additional regex pattern for invalid characters.
            custom_error: Error message for custom pattern violations.
        """
        self._blacklist = set(name.lower() for name in (blacklist or []))
        self._allow_empty = allow_empty
        self._max_length = max_length
        self._custom_pattern = custom_pattern
        self._custom_error = custom_error or "Input contains invalid characters."

        # Compile the invalid chars pattern
        self._invalid_pattern = re.compile(self.INVALID_CHARS_PATTERN)

    def validate(self, text: str) -> ValidationResult:
        """
        Validate the input text.

        Args:
            text: The text to validate.

        Returns:
            ValidationResult with is_valid and optional error_message.
        """
        text = text.strip()

        # Check empty
        if not text:
            if self._allow_empty:
                return ValidationResult(is_valid=True)
            return ValidationResult(
                is_valid=False,
                error_message="Name cannot be empty.",
            )

        # Check length
        if len(text) > self._max_length:
            return ValidationResult(
                is_valid=False,
                error_message=f"Name is too long (max {self._max_length} characters).",
            )

        # Check invalid characters
        invalid_match = self._invalid_pattern.search(text)
        if invalid_match:
            invalid_chars = set(self._invalid_pattern.findall(text))
            # Filter out control characters for display
            display_chars = [c for c in invalid_chars if ord(c) >= 32]
            if display_chars:
                chars_str = ", ".join(f"'{c}'" for c in display_chars)
                return ValidationResult(
                    is_valid=False,
                    error_message=f"Invalid characters: {chars_str}",
                )
            return ValidationResult(
                is_valid=False,
                error_message="Control characters are not allowed.",
            )

        # Check custom pattern
        if self._custom_pattern:
            if re.search(self._custom_pattern, text):
                return ValidationResult(
                    is_valid=False,
                    error_message=self._custom_error,
                )

        # Check reserved names
        if text.upper() in self.RESERVED_NAMES:
            return ValidationResult(
                is_valid=False,
                error_message=f"'{text}' is a reserved system name.",
            )

        # Check trailing period (Windows issue)
        if text.endswith("."):
            return ValidationResult(
                is_valid=False,
                error_message="Name cannot end with a period.",
            )

        # Check blacklist (case-insensitive)
        if text.lower() in self._blacklist:
            return ValidationResult(
                is_valid=False,
                error_message=f"'{text}' is already in use or not allowed.",
            )

        return ValidationResult(is_valid=True)

    def add_to_blacklist(self, name: str) -> None:
        """
        Add a name to the blacklist.

        Args:
            name: Name to blacklist.
        """
        self._blacklist.add(name.lower())

    def remove_from_blacklist(self, name: str) -> None:
        """
        Remove a name from the blacklist.

        Args:
            name: Name to remove.
        """
        self._blacklist.discard(name.lower())

    def clear_blacklist(self) -> None:
        """Clear the blacklist."""
        self._blacklist.clear()
__init__(blacklist=None, allow_empty=False, max_length=255, custom_pattern=None, custom_error=None)

Initialize the validator.

Parameters:

Name Type Description Default
blacklist List[str] | None

List of names that are not allowed.

None
allow_empty bool

Whether empty input is allowed.

False
max_length int

Maximum allowed length.

255
custom_pattern str | None

Additional regex pattern for invalid characters.

None
custom_error str | None

Error message for custom pattern violations.

None
Source code in src\shared_services\prompt_dialogs\input\validators.py
def __init__(
    self,
    blacklist: List[str] | None = None,
    allow_empty: bool = False,
    max_length: int = 255,
    custom_pattern: str | None = None,
    custom_error: str | None = None,
) -> None:
    """
    Initialize the validator.

    Args:
        blacklist: List of names that are not allowed.
        allow_empty: Whether empty input is allowed.
        max_length: Maximum allowed length.
        custom_pattern: Additional regex pattern for invalid characters.
        custom_error: Error message for custom pattern violations.
    """
    self._blacklist = set(name.lower() for name in (blacklist or []))
    self._allow_empty = allow_empty
    self._max_length = max_length
    self._custom_pattern = custom_pattern
    self._custom_error = custom_error or "Input contains invalid characters."

    # Compile the invalid chars pattern
    self._invalid_pattern = re.compile(self.INVALID_CHARS_PATTERN)
add_to_blacklist(name)

Add a name to the blacklist.

Parameters:

Name Type Description Default
name str

Name to blacklist.

required
Source code in src\shared_services\prompt_dialogs\input\validators.py
def add_to_blacklist(self, name: str) -> None:
    """
    Add a name to the blacklist.

    Args:
        name: Name to blacklist.
    """
    self._blacklist.add(name.lower())
clear_blacklist()

Clear the blacklist.

Source code in src\shared_services\prompt_dialogs\input\validators.py
def clear_blacklist(self) -> None:
    """Clear the blacklist."""
    self._blacklist.clear()
remove_from_blacklist(name)

Remove a name from the blacklist.

Parameters:

Name Type Description Default
name str

Name to remove.

required
Source code in src\shared_services\prompt_dialogs\input\validators.py
def remove_from_blacklist(self, name: str) -> None:
    """
    Remove a name from the blacklist.

    Args:
        name: Name to remove.
    """
    self._blacklist.discard(name.lower())
validate(text)

Validate the input text.

Parameters:

Name Type Description Default
text str

The text to validate.

required

Returns:

Type Description
ValidationResult

ValidationResult with is_valid and optional error_message.

Source code in src\shared_services\prompt_dialogs\input\validators.py
def validate(self, text: str) -> ValidationResult:
    """
    Validate the input text.

    Args:
        text: The text to validate.

    Returns:
        ValidationResult with is_valid and optional error_message.
    """
    text = text.strip()

    # Check empty
    if not text:
        if self._allow_empty:
            return ValidationResult(is_valid=True)
        return ValidationResult(
            is_valid=False,
            error_message="Name cannot be empty.",
        )

    # Check length
    if len(text) > self._max_length:
        return ValidationResult(
            is_valid=False,
            error_message=f"Name is too long (max {self._max_length} characters).",
        )

    # Check invalid characters
    invalid_match = self._invalid_pattern.search(text)
    if invalid_match:
        invalid_chars = set(self._invalid_pattern.findall(text))
        # Filter out control characters for display
        display_chars = [c for c in invalid_chars if ord(c) >= 32]
        if display_chars:
            chars_str = ", ".join(f"'{c}'" for c in display_chars)
            return ValidationResult(
                is_valid=False,
                error_message=f"Invalid characters: {chars_str}",
            )
        return ValidationResult(
            is_valid=False,
            error_message="Control characters are not allowed.",
        )

    # Check custom pattern
    if self._custom_pattern:
        if re.search(self._custom_pattern, text):
            return ValidationResult(
                is_valid=False,
                error_message=self._custom_error,
            )

    # Check reserved names
    if text.upper() in self.RESERVED_NAMES:
        return ValidationResult(
            is_valid=False,
            error_message=f"'{text}' is a reserved system name.",
        )

    # Check trailing period (Windows issue)
    if text.endswith("."):
        return ValidationResult(
            is_valid=False,
            error_message="Name cannot end with a period.",
        )

    # Check blacklist (case-insensitive)
    if text.lower() in self._blacklist:
        return ValidationResult(
            is_valid=False,
            error_message=f"'{text}' is already in use or not allowed.",
        )

    return ValidationResult(is_valid=True)

InterceptionEntry dataclass

Record of an intercepted message box.

Attributes:

Name Type Description
timestamp datetime

When the interception occurred.

message_type MessageBoxType

Type of message box.

title str

Message box title.

message str

Message box text content.

source_file Optional[str]

File where the message box was triggered.

source_line Optional[int]

Line number in the source file.

call_stack str

Full call stack at interception time.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@dataclass
class InterceptionEntry:
    """
    Record of an intercepted message box.

    Attributes:
        timestamp: When the interception occurred.
        message_type: Type of message box.
        title: Message box title.
        message: Message box text content.
        source_file: File where the message box was triggered.
        source_line: Line number in the source file.
        call_stack: Full call stack at interception time.
    """

    timestamp: datetime
    message_type: MessageBoxType
    title: str
    message: str
    source_file: Optional[str] = None
    source_line: Optional[int] = None
    call_stack: str = ""

InterceptionLog

Singleton log for tracking intercepted message boxes.

Maintains a circular buffer of recent interceptions with call stack information to help developers identify problematic code.

Example

Logging an interception::

InterceptionLog.log(
    message_type=MessageBoxType.WARNING,
    title="Error",
    message="Failed to save",
)

Reviewing entries::

for entry in InterceptionLog.get_recent_entries():
    print(f"[{entry.timestamp}] {entry.title}")
Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
class InterceptionLog:
    """
    Singleton log for tracking intercepted message boxes.

    Maintains a circular buffer of recent interceptions with call stack
    information to help developers identify problematic code.

    Example:
        Logging an interception::

            InterceptionLog.log(
                message_type=MessageBoxType.WARNING,
                title="Error",
                message="Failed to save",
            )

        Reviewing entries::

            for entry in InterceptionLog.get_recent_entries():
                print(f"[{entry.timestamp}] {entry.title}")
    """

    _entries: ClassVar[List[InterceptionEntry]] = []
    _max_entries: ClassVar[int] = 100
    _enabled: ClassVar[bool] = True

    @classmethod
    def log(
        cls,
        message_type: MessageBoxType,
        title: str,
        message: str,
        capture_stack: bool = True,
    ) -> InterceptionEntry:
        """
        Log an intercepted message box.

        Args:
            message_type: Type of message box.
            title: Message box title.
            message: Message box content.
            capture_stack: Whether to capture the call stack.

        Returns:
            The created InterceptionEntry.
        """
        if not cls._enabled:
            return InterceptionEntry(
                timestamp=datetime.now(),
                message_type=message_type,
                title=title,
                message=message,
            )

        # Capture call stack
        call_stack = ""
        source_file = None
        source_line = None

        if capture_stack:
            stack = traceback.extract_stack()
            # Find the first frame outside of the dialogs module
            for frame in reversed(stack[:-3]):  # Skip the last 3 frames (this function)
                if "shared_services/dialogs" not in frame.filename:
                    source_file = frame.filename
                    source_line = frame.lineno
                    break

            call_stack = "".join(traceback.format_stack()[:-3])

        entry = InterceptionEntry(
            timestamp=datetime.now(),
            message_type=message_type,
            title=title,
            message=message,
            source_file=source_file,
            source_line=source_line,
            call_stack=call_stack,
        )

        # Add to log (circular buffer)
        cls._entries.append(entry)
        if len(cls._entries) > cls._max_entries:
            cls._entries.pop(0)

        # Log to application logger
        cls._log_to_app_logger(entry)

        return entry

    @classmethod
    def get_recent_entries(cls, count: int = 10) -> List[InterceptionEntry]:
        """
        Get the most recent interception entries.

        Args:
            count: Maximum number of entries to return.

        Returns:
            List of recent InterceptionEntry objects.
        """
        return list(cls._entries[-count:])

    @classmethod
    def get_all_entries(cls) -> List[InterceptionEntry]:
        """
        Get all logged interception entries.

        Returns:
            List of all InterceptionEntry objects.
        """
        return list(cls._entries)

    @classmethod
    def clear(cls) -> None:
        """Clear all logged entries."""
        cls._entries.clear()

    @classmethod
    def set_max_entries(cls, max_entries: int) -> None:
        """
        Set the maximum number of entries to keep.

        Args:
            max_entries: Maximum entry count.
        """
        cls._max_entries = max_entries
        # Trim if needed
        while len(cls._entries) > cls._max_entries:
            cls._entries.pop(0)

    @classmethod
    def enable(cls) -> None:
        """Enable interception logging."""
        cls._enabled = True

    @classmethod
    def disable(cls) -> None:
        """Disable interception logging."""
        cls._enabled = False

    @classmethod
    def is_enabled(cls) -> bool:
        """
        Check if logging is enabled.

        Returns:
            True if logging is enabled.
        """
        return cls._enabled

    @classmethod
    def get_entries_by_type(
        cls,
        message_type: MessageBoxType,
    ) -> List[InterceptionEntry]:
        """
        Get entries filtered by message type.

        Args:
            message_type: Type to filter by.

        Returns:
            List of matching entries.
        """
        return [e for e in cls._entries if e.message_type == message_type]

    @classmethod
    def get_entries_by_source(cls, source_pattern: str) -> List[InterceptionEntry]:
        """
        Get entries filtered by source file pattern.

        Args:
            source_pattern: Substring to match in source file path.

        Returns:
            List of matching entries.
        """
        return [
            e
            for e in cls._entries
            if e.source_file and source_pattern in e.source_file
        ]

    @classmethod
    def format_entry(cls, entry: InterceptionEntry) -> str:
        """
        Format an entry for display.

        Args:
            entry: The entry to format.

        Returns:
            Formatted string representation.
        """
        lines = [
            f"[{entry.message_type.name}] MessageBox intercepted during loading:",
            f'  Title: "{entry.title}"',
            f'  Message: "{entry.message[:100]}..."'
            if len(entry.message) > 100
            else f'  Message: "{entry.message}"',
        ]

        if entry.source_file:
            lines.append(f"  Source: {entry.source_file}:{entry.source_line}")

        if entry.call_stack:
            lines.append("  Call stack:")
            for line in entry.call_stack.strip().split("\n")[-6:]:
                lines.append(f"    {line.strip()}")

        return "\n".join(lines)

    @classmethod
    def _log_to_app_logger(cls, entry: InterceptionEntry) -> None:
        """
        Log entry to the application logger.

        Args:
            entry: Entry to log.
        """
        try:
            from src.shared_services.logging.logger_factory import get_logger

            logger = get_logger()
            formatted = cls.format_entry(entry)
            logger.warning(formatted)
        except ImportError:
            pass
clear() classmethod

Clear all logged entries.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def clear(cls) -> None:
    """Clear all logged entries."""
    cls._entries.clear()
disable() classmethod

Disable interception logging.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def disable(cls) -> None:
    """Disable interception logging."""
    cls._enabled = False
enable() classmethod

Enable interception logging.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def enable(cls) -> None:
    """Enable interception logging."""
    cls._enabled = True
format_entry(entry) classmethod

Format an entry for display.

Parameters:

Name Type Description Default
entry InterceptionEntry

The entry to format.

required

Returns:

Type Description
str

Formatted string representation.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def format_entry(cls, entry: InterceptionEntry) -> str:
    """
    Format an entry for display.

    Args:
        entry: The entry to format.

    Returns:
        Formatted string representation.
    """
    lines = [
        f"[{entry.message_type.name}] MessageBox intercepted during loading:",
        f'  Title: "{entry.title}"',
        f'  Message: "{entry.message[:100]}..."'
        if len(entry.message) > 100
        else f'  Message: "{entry.message}"',
    ]

    if entry.source_file:
        lines.append(f"  Source: {entry.source_file}:{entry.source_line}")

    if entry.call_stack:
        lines.append("  Call stack:")
        for line in entry.call_stack.strip().split("\n")[-6:]:
            lines.append(f"    {line.strip()}")

    return "\n".join(lines)
get_all_entries() classmethod

Get all logged interception entries.

Returns:

Type Description
List[InterceptionEntry]

List of all InterceptionEntry objects.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def get_all_entries(cls) -> List[InterceptionEntry]:
    """
    Get all logged interception entries.

    Returns:
        List of all InterceptionEntry objects.
    """
    return list(cls._entries)
get_entries_by_source(source_pattern) classmethod

Get entries filtered by source file pattern.

Parameters:

Name Type Description Default
source_pattern str

Substring to match in source file path.

required

Returns:

Type Description
List[InterceptionEntry]

List of matching entries.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def get_entries_by_source(cls, source_pattern: str) -> List[InterceptionEntry]:
    """
    Get entries filtered by source file pattern.

    Args:
        source_pattern: Substring to match in source file path.

    Returns:
        List of matching entries.
    """
    return [
        e
        for e in cls._entries
        if e.source_file and source_pattern in e.source_file
    ]
get_entries_by_type(message_type) classmethod

Get entries filtered by message type.

Parameters:

Name Type Description Default
message_type MessageBoxType

Type to filter by.

required

Returns:

Type Description
List[InterceptionEntry]

List of matching entries.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def get_entries_by_type(
    cls,
    message_type: MessageBoxType,
) -> List[InterceptionEntry]:
    """
    Get entries filtered by message type.

    Args:
        message_type: Type to filter by.

    Returns:
        List of matching entries.
    """
    return [e for e in cls._entries if e.message_type == message_type]
get_recent_entries(count=10) classmethod

Get the most recent interception entries.

Parameters:

Name Type Description Default
count int

Maximum number of entries to return.

10

Returns:

Type Description
List[InterceptionEntry]

List of recent InterceptionEntry objects.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def get_recent_entries(cls, count: int = 10) -> List[InterceptionEntry]:
    """
    Get the most recent interception entries.

    Args:
        count: Maximum number of entries to return.

    Returns:
        List of recent InterceptionEntry objects.
    """
    return list(cls._entries[-count:])
is_enabled() classmethod

Check if logging is enabled.

Returns:

Type Description
bool

True if logging is enabled.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def is_enabled(cls) -> bool:
    """
    Check if logging is enabled.

    Returns:
        True if logging is enabled.
    """
    return cls._enabled
log(message_type, title, message, capture_stack=True) classmethod

Log an intercepted message box.

Parameters:

Name Type Description Default
message_type MessageBoxType

Type of message box.

required
title str

Message box title.

required
message str

Message box content.

required
capture_stack bool

Whether to capture the call stack.

True

Returns:

Type Description
InterceptionEntry

The created InterceptionEntry.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def log(
    cls,
    message_type: MessageBoxType,
    title: str,
    message: str,
    capture_stack: bool = True,
) -> InterceptionEntry:
    """
    Log an intercepted message box.

    Args:
        message_type: Type of message box.
        title: Message box title.
        message: Message box content.
        capture_stack: Whether to capture the call stack.

    Returns:
        The created InterceptionEntry.
    """
    if not cls._enabled:
        return InterceptionEntry(
            timestamp=datetime.now(),
            message_type=message_type,
            title=title,
            message=message,
        )

    # Capture call stack
    call_stack = ""
    source_file = None
    source_line = None

    if capture_stack:
        stack = traceback.extract_stack()
        # Find the first frame outside of the dialogs module
        for frame in reversed(stack[:-3]):  # Skip the last 3 frames (this function)
            if "shared_services/dialogs" not in frame.filename:
                source_file = frame.filename
                source_line = frame.lineno
                break

        call_stack = "".join(traceback.format_stack()[:-3])

    entry = InterceptionEntry(
        timestamp=datetime.now(),
        message_type=message_type,
        title=title,
        message=message,
        source_file=source_file,
        source_line=source_line,
        call_stack=call_stack,
    )

    # Add to log (circular buffer)
    cls._entries.append(entry)
    if len(cls._entries) > cls._max_entries:
        cls._entries.pop(0)

    # Log to application logger
    cls._log_to_app_logger(entry)

    return entry
set_max_entries(max_entries) classmethod

Set the maximum number of entries to keep.

Parameters:

Name Type Description Default
max_entries int

Maximum entry count.

required
Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
@classmethod
def set_max_entries(cls, max_entries: int) -> None:
    """
    Set the maximum number of entries to keep.

    Args:
        max_entries: Maximum entry count.
    """
    cls._max_entries = max_entries
    # Trim if needed
    while len(cls._entries) > cls._max_entries:
        cls._entries.pop(0)

InterceptionManager

Bases: QObject

Manages QMessageBox interception during loading operations.

Replaces QMessageBox methods with intercepting versions that route messages through a callback (typically to display within a loading dialog). All interceptions are logged for developer monitoring.

Signals

intercepted: Emitted when a message box is intercepted.

Example

Using with a loading dialog::

def handle_intercept(title, message, icon, buttons):
    return loading_dialog.show_embedded_message(
        title, message, icon, buttons
    )

manager = InterceptionManager(on_intercept=handle_intercept)
manager.start()
# ... loading work ...
manager.stop()
Source code in src\shared_services\prompt_dialogs\interception\interceptor.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
class InterceptionManager(QObject):
    """
    Manages QMessageBox interception during loading operations.

    Replaces QMessageBox methods with intercepting versions that route
    messages through a callback (typically to display within a loading dialog).
    All interceptions are logged for developer monitoring.

    Signals:
        intercepted: Emitted when a message box is intercepted.

    Example:
        Using with a loading dialog::

            def handle_intercept(title, message, icon, buttons):
                return loading_dialog.show_embedded_message(
                    title, message, icon, buttons
                )

            manager = InterceptionManager(on_intercept=handle_intercept)
            manager.start()
            # ... loading work ...
            manager.stop()
    """

    intercepted: Signal = Signal(str, str, int, int)  # title, message, icon, buttons

    def __init__(
        self,
        on_intercept: Optional[InterceptCallback] = None,
        parent: Optional[QObject] = None,
    ) -> None:
        """
        Initialize the interception manager.

        Args:
            on_intercept: Callback to handle intercepted messages.
            parent: Parent QObject.
        """
        super().__init__(parent)

        self._on_intercept = on_intercept
        self._is_intercepting = False

        # Store original QMessageBox methods
        self._original_exec: Optional[Callable] = None
        self._original_information: Optional[Callable] = None
        self._original_warning: Optional[Callable] = None
        self._original_critical: Optional[Callable] = None
        self._original_question: Optional[Callable] = None

        # Store original QInputDialog methods
        self._original_getText: Optional[Callable] = None
        self._original_getItem: Optional[Callable] = None
        self._original_getInt: Optional[Callable] = None

    def set_callback(self, callback: InterceptCallback) -> None:
        """
        Set the interception callback.

        Args:
            callback: Function to handle intercepted messages.
        """
        self._on_intercept = callback

    def is_active(self) -> bool:
        """
        Check if interception is currently active.

        Returns:
            True if intercepting.
        """
        return self._is_intercepting

    @Slot()
    def start(self) -> None:
        """
        Start intercepting QMessageBox and QInputDialog calls.

        Replaces QMessageBox and QInputDialog static methods with
        intercepting versions. Safe to call multiple times.
        """
        if self._is_intercepting:
            return

        # Store original QMessageBox methods
        self._original_exec = QMessageBox.exec
        self._original_information = QMessageBox.information
        self._original_warning = QMessageBox.warning
        self._original_critical = QMessageBox.critical
        self._original_question = QMessageBox.question

        # Apply QMessageBox patches
        QMessageBox.exec = self._intercept_exec
        QMessageBox.information = self._intercept_information
        QMessageBox.warning = self._intercept_warning
        QMessageBox.critical = self._intercept_critical
        QMessageBox.question = self._intercept_question

        # Store original QInputDialog methods
        self._original_getText = QInputDialog.getText
        self._original_getItem = QInputDialog.getItem
        self._original_getInt = QInputDialog.getInt

        # Apply QInputDialog patches
        QInputDialog.getText = self._intercept_getText
        QInputDialog.getItem = self._intercept_getItem
        QInputDialog.getInt = self._intercept_getInt

        self._is_intercepting = True

    @Slot()
    def stop(self) -> None:
        """
        Stop intercepting QMessageBox and QInputDialog calls.

        Restores original QMessageBox and QInputDialog methods.
        Safe to call multiple times.
        """
        if not self._is_intercepting:
            return

        # Restore original QMessageBox methods
        if self._original_exec is not None:
            QMessageBox.exec = self._original_exec
        if self._original_information is not None:
            QMessageBox.information = self._original_information
        if self._original_warning is not None:
            QMessageBox.warning = self._original_warning
        if self._original_critical is not None:
            QMessageBox.critical = self._original_critical
        if self._original_question is not None:
            QMessageBox.question = self._original_question

        # Restore original QInputDialog methods
        if self._original_getText is not None:
            QInputDialog.getText = self._original_getText
        if self._original_getItem is not None:
            QInputDialog.getItem = self._original_getItem
        if self._original_getInt is not None:
            QInputDialog.getInt = self._original_getInt

        self._is_intercepting = False

    def _intercept_exec(self, msgbox_self: Optional[QMessageBox] = None) -> int:
        """
        Intercept QMessageBox.exec() calls.

        Args:
            msgbox_self: The QMessageBox instance.

        Returns:
            The button result.
        """
        # Handle both bound and unbound calls
        if msgbox_self is None:
            msgbox_self = self._find_messagebox_in_caller()

        if msgbox_self is None:
            return QMessageBox.StandardButton.Ok.value

        try:
            title = msgbox_self.windowTitle() or "Information"
            text = msgbox_self.text() or ""
            icon = (
                msgbox_self.icon()
                if hasattr(msgbox_self, "icon")
                else QMessageBox.Icon.Information
            )
            buttons = (
                msgbox_self.standardButtons()
                if hasattr(msgbox_self, "standardButtons")
                else QMessageBox.StandardButton.Ok
            )

            # Default to Ok if no buttons are set on the QMessageBox
            if buttons == QMessageBox.StandardButton.NoButton:
                buttons = QMessageBox.StandardButton.Ok

            return self._handle_interception(title, text, icon, buttons)

        except (RuntimeError, AttributeError):
            # Fall back to original
            if self._original_exec is not None:
                return self._original_exec(msgbox_self)
            return QMessageBox.StandardButton.Ok.value

    def _intercept_information(
        self,
        parent,
        title: str,
        text: str,
        buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
        default_button: QMessageBox.StandardButton = QMessageBox.StandardButton.NoButton,
    ) -> int:
        """Intercept QMessageBox.information() calls."""
        return self._handle_interception(
            title,
            text,
            QMessageBox.Icon.Information,
            buttons,
        )

    def _intercept_warning(
        self,
        parent,
        title: str,
        text: str,
        buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
        default_button: QMessageBox.StandardButton = QMessageBox.StandardButton.NoButton,
    ) -> int:
        """Intercept QMessageBox.warning() calls."""
        return self._handle_interception(
            title,
            text,
            QMessageBox.Icon.Warning,
            buttons,
        )

    def _intercept_critical(
        self,
        parent,
        title: str,
        text: str,
        buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
        default_button: QMessageBox.StandardButton = QMessageBox.StandardButton.NoButton,
    ) -> int:
        """Intercept QMessageBox.critical() calls."""
        return self._handle_interception(
            title,
            text,
            QMessageBox.Icon.Critical,
            buttons,
        )

    def _intercept_question(
        self,
        parent,
        title: str,
        text: str,
        buttons: QMessageBox.StandardButton = (
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        ),
        default_button: QMessageBox.StandardButton = QMessageBox.StandardButton.NoButton,
    ) -> int:
        """Intercept QMessageBox.question() calls."""
        return self._handle_interception(
            title,
            text,
            QMessageBox.Icon.Question,
            buttons,
        )

    def _handle_interception(
        self,
        title: str,
        text: str,
        icon: QMessageBox.Icon,
        buttons: QMessageBox.StandardButton,
    ) -> int:
        """
        Handle an intercepted message box.

        Args:
            title: Message title.
            text: Message text.
            icon: Message icon type.
            buttons: Standard buttons.

        Returns:
            The selected button result.
        """
        # Log the interception
        message_type = self._icon_to_message_type(icon)
        InterceptionLog.log(
            message_type=message_type,
            title=title,
            message=text,
            capture_stack=True,
        )

        # Emit signal (convert enums to int using .value for PySide6 compatibility)
        icon_value = icon.value if hasattr(icon, 'value') else 0
        buttons_value = buttons.value if hasattr(buttons, 'value') else 0
        self.intercepted.emit(title, text, icon_value, buttons_value)

        # Call callback if set
        if self._on_intercept is not None:
            try:
                result = self._on_intercept(title, text, icon, buttons)
                # Convert result to int (handle PySide6 enum)
                return result.value if hasattr(result, 'value') else int(result)
            except (RuntimeError, TypeError):
                pass

        # Default to OK
        return QMessageBox.StandardButton.Ok.value

    # =========================================================================
    # INPUT DIALOG INTERCEPTION
    # =========================================================================

    def _intercept_getText(
        self,
        parent,
        title: str,
        label: str,
        echo: QLineEdit.EchoMode = QLineEdit.EchoMode.Normal,
        text: str = "",
        *args,
        **kwargs,
    ) -> Tuple[str, bool]:
        """
        Intercept QInputDialog.getText() calls.

        Routes to our styled InputDialog instead.
        """
        # Log the interception
        InterceptionLog.log(
            message_type=MessageBoxType.QUESTION,
            title=title,
            message=f"Input requested: {label}",
            capture_stack=True,
        )

        # This is a basic interception - the LoadingDialogInterceptor
        # will override this to hide loading and show InputDialog
        if self._on_intercept is not None:
            # For input dialogs, we handle it specially in LoadingDialogInterceptor
            pass

        # Default: call original if available
        if self._original_getText is not None:
            return self._original_getText(parent, title, label, echo, text, *args, **kwargs)

        return "", False

    def _intercept_getItem(
        self,
        parent,
        title: str,
        label: str,
        items,
        current: int = 0,
        editable: bool = True,
        *args,
        **kwargs,
    ) -> Tuple[str, bool]:
        """
        Intercept QInputDialog.getItem() calls.

        Routes to our styled selection dialog.
        """
        # Log the interception
        InterceptionLog.log(
            message_type=MessageBoxType.QUESTION,
            title=title,
            message=f"Item selection requested: {label}",
            capture_stack=True,
        )

        # Default: call original if available
        if self._original_getItem is not None:
            return self._original_getItem(parent, title, label, items, current, editable, *args, **kwargs)

        return "", False

    def _intercept_getInt(
        self,
        parent,
        title: str,
        label: str,
        value: int = 0,
        minValue: int = -2147483647,
        maxValue: int = 2147483647,
        step: int = 1,
        *args,
        **kwargs,
    ) -> Tuple[int, bool]:
        """
        Intercept QInputDialog.getInt() calls.

        Routes to our styled input dialog.
        """
        # Log the interception
        InterceptionLog.log(
            message_type=MessageBoxType.QUESTION,
            title=title,
            message=f"Integer input requested: {label}",
            capture_stack=True,
        )

        # Default: call original if available
        if self._original_getInt is not None:
            return self._original_getInt(parent, title, label, value, minValue, maxValue, step, *args, **kwargs)

        return 0, False

    def _find_messagebox_in_caller(self) -> Optional[QMessageBox]:
        """
        Try to find a QMessageBox instance in the caller's local variables.

        Returns:
            QMessageBox instance if found, None otherwise.
        """
        try:
            frame = inspect.currentframe()
            if frame is None:
                return None

            try:
                caller_locals = frame.f_back.f_back.f_locals if frame.f_back else {}
                for value in caller_locals.values():
                    if isinstance(value, QMessageBox):
                        return value
            finally:
                del frame

        except (AttributeError, RuntimeError):
            pass

        return None

    def _icon_to_message_type(self, icon: QMessageBox.Icon) -> MessageBoxType:
        """
        Convert QMessageBox.Icon to MessageBoxType.

        Args:
            icon: The QMessageBox icon.

        Returns:
            Corresponding MessageBoxType.
        """
        mapping = {
            QMessageBox.Icon.Information: MessageBoxType.INFORMATION,
            QMessageBox.Icon.Warning: MessageBoxType.WARNING,
            QMessageBox.Icon.Critical: MessageBoxType.CRITICAL,
            QMessageBox.Icon.Question: MessageBoxType.QUESTION,
        }
        return mapping.get(icon, MessageBoxType.UNKNOWN)
__init__(on_intercept=None, parent=None)

Initialize the interception manager.

Parameters:

Name Type Description Default
on_intercept Optional[InterceptCallback]

Callback to handle intercepted messages.

None
parent Optional[QObject]

Parent QObject.

None
Source code in src\shared_services\prompt_dialogs\interception\interceptor.py
def __init__(
    self,
    on_intercept: Optional[InterceptCallback] = None,
    parent: Optional[QObject] = None,
) -> None:
    """
    Initialize the interception manager.

    Args:
        on_intercept: Callback to handle intercepted messages.
        parent: Parent QObject.
    """
    super().__init__(parent)

    self._on_intercept = on_intercept
    self._is_intercepting = False

    # Store original QMessageBox methods
    self._original_exec: Optional[Callable] = None
    self._original_information: Optional[Callable] = None
    self._original_warning: Optional[Callable] = None
    self._original_critical: Optional[Callable] = None
    self._original_question: Optional[Callable] = None

    # Store original QInputDialog methods
    self._original_getText: Optional[Callable] = None
    self._original_getItem: Optional[Callable] = None
    self._original_getInt: Optional[Callable] = None
is_active()

Check if interception is currently active.

Returns:

Type Description
bool

True if intercepting.

Source code in src\shared_services\prompt_dialogs\interception\interceptor.py
def is_active(self) -> bool:
    """
    Check if interception is currently active.

    Returns:
        True if intercepting.
    """
    return self._is_intercepting
set_callback(callback)

Set the interception callback.

Parameters:

Name Type Description Default
callback InterceptCallback

Function to handle intercepted messages.

required
Source code in src\shared_services\prompt_dialogs\interception\interceptor.py
def set_callback(self, callback: InterceptCallback) -> None:
    """
    Set the interception callback.

    Args:
        callback: Function to handle intercepted messages.
    """
    self._on_intercept = callback
start()

Start intercepting QMessageBox and QInputDialog calls.

Replaces QMessageBox and QInputDialog static methods with intercepting versions. Safe to call multiple times.

Source code in src\shared_services\prompt_dialogs\interception\interceptor.py
@Slot()
def start(self) -> None:
    """
    Start intercepting QMessageBox and QInputDialog calls.

    Replaces QMessageBox and QInputDialog static methods with
    intercepting versions. Safe to call multiple times.
    """
    if self._is_intercepting:
        return

    # Store original QMessageBox methods
    self._original_exec = QMessageBox.exec
    self._original_information = QMessageBox.information
    self._original_warning = QMessageBox.warning
    self._original_critical = QMessageBox.critical
    self._original_question = QMessageBox.question

    # Apply QMessageBox patches
    QMessageBox.exec = self._intercept_exec
    QMessageBox.information = self._intercept_information
    QMessageBox.warning = self._intercept_warning
    QMessageBox.critical = self._intercept_critical
    QMessageBox.question = self._intercept_question

    # Store original QInputDialog methods
    self._original_getText = QInputDialog.getText
    self._original_getItem = QInputDialog.getItem
    self._original_getInt = QInputDialog.getInt

    # Apply QInputDialog patches
    QInputDialog.getText = self._intercept_getText
    QInputDialog.getItem = self._intercept_getItem
    QInputDialog.getInt = self._intercept_getInt

    self._is_intercepting = True
stop()

Stop intercepting QMessageBox and QInputDialog calls.

Restores original QMessageBox and QInputDialog methods. Safe to call multiple times.

Source code in src\shared_services\prompt_dialogs\interception\interceptor.py
@Slot()
def stop(self) -> None:
    """
    Stop intercepting QMessageBox and QInputDialog calls.

    Restores original QMessageBox and QInputDialog methods.
    Safe to call multiple times.
    """
    if not self._is_intercepting:
        return

    # Restore original QMessageBox methods
    if self._original_exec is not None:
        QMessageBox.exec = self._original_exec
    if self._original_information is not None:
        QMessageBox.information = self._original_information
    if self._original_warning is not None:
        QMessageBox.warning = self._original_warning
    if self._original_critical is not None:
        QMessageBox.critical = self._original_critical
    if self._original_question is not None:
        QMessageBox.question = self._original_question

    # Restore original QInputDialog methods
    if self._original_getText is not None:
        QInputDialog.getText = self._original_getText
    if self._original_getItem is not None:
        QInputDialog.getItem = self._original_getItem
    if self._original_getInt is not None:
        QInputDialog.getInt = self._original_getInt

    self._is_intercepting = False

LoadingDialog

Bases: ThemeAwareDialog

Modal loading dialog with progress tracking.

Provides a clean, themed loading dialog with: - Animated spinner - Progress bar with percentage - Subprocess status tracking - Message box interception (hides loading, shows MessageDialog) - Fail-safe close button after timeout - Context manager support

Signals

cancelled: Emitted when the user cancels via fail-safe button. progress_updated: Emitted when progress changes (message, percentage). subprocess_changed: Emitted when subprocess name changes.

Example

Using as context manager::

with LoadingDialog(parent) as loading:
    loading.show_loading("Uploading...")
    for i, item in enumerate(items):
        loading.update_progress(f"Item {i+1}/{len(items)}",
                                int(100 * i / len(items)))
        process_item(item)
Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
class LoadingDialog(ThemeAwareDialog):
    """
    Modal loading dialog with progress tracking.

    Provides a clean, themed loading dialog with:
    - Animated spinner
    - Progress bar with percentage
    - Subprocess status tracking
    - Message box interception (hides loading, shows MessageDialog)
    - Fail-safe close button after timeout
    - Context manager support

    Signals:
        cancelled: Emitted when the user cancels via fail-safe button.
        progress_updated: Emitted when progress changes (message, percentage).
        subprocess_changed: Emitted when subprocess name changes.

    Example:
        Using as context manager::

            with LoadingDialog(parent) as loading:
                loading.show_loading("Uploading...")
                for i, item in enumerate(items):
                    loading.update_progress(f"Item {i+1}/{len(items)}",
                                            int(100 * i / len(items)))
                    process_item(item)
    """

    cancelled: Signal = Signal()
    progress_updated: Signal = Signal(str, int)
    subprocess_changed: Signal = Signal(str)

    # Size constraints
    MIN_WIDTH = 400 + 2 * ThemeAwareDialog._SHADOW_MARGIN
    MAX_WIDTH = 480 + 2 * ThemeAwareDialog._SHADOW_MARGIN

    def __init__(self, parent: Optional[QWidget] = None) -> None:
        """
        Initialize the loading dialog.

        Args:
            parent: Parent widget. The dialog will automatically use the
                top-level window (main window) as its actual parent to ensure
                stability when views are being loaded/replaced.
        """
        # Find the top-level window to use as stable parent
        # This prevents issues when the original parent gets replaced during loading
        stable_parent = self._find_top_level_window(parent)

        super().__init__(
            parent=stable_parent,
            stylesheets=[DialogStylesheets.Main, DialogStylesheets.LoadingDialog],
        )

        # Store original parent for centering purposes
        self._centering_widget = parent

        # State
        self._current_subprocess = ""
        self._current_progress = -1
        self._context_manager_active = False

        # Setup
        self._setup_ui()
        self._setup_interception()
        self._setup_timers()

        # Register with dialog registry
        DialogRegistry.instance().register(self, "loading")

    def _setup_ui(self) -> None:
        """Set up the dialog UI."""
        self.setObjectName("LoadingDialog")
        self.setWindowFlags(
            Qt.WindowType.Dialog
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
            | Qt.WindowType.NoDropShadowWindowHint
        )
        self.setModal(True)

        # Set accent color from palette highlight
        try:
            palette = QApplication.instance().palette()
            self._accent_color = palette.color(QPalette.ColorRole.Highlight)
        except (RuntimeError, AttributeError):
            self._accent_color = QColor(37, 99, 235)

        # Width constraints only - let height be dynamic based on content
        self.setMinimumWidth(self.MIN_WIDTH)
        self.setMaximumWidth(self.MAX_WIDTH)

        # Container frame
        self._container = QFrame(self)
        self._container.setObjectName("LoadingDialog_Container")
        self._container.setSizePolicy(
            QSizePolicy.Policy.MinimumExpanding,
            QSizePolicy.Policy.MinimumExpanding,
        )

        # Main layout - compact spacing
        container_layout = QVBoxLayout(self._container)
        container_layout.setSpacing(16)
        container_layout.setContentsMargins(24, 20, 24, 20)
        container_layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)

        # Loading section
        self._loading_widget = QWidget()
        self._loading_widget.setObjectName("LoadingDialog_LoadingSection")
        loading_layout = QVBoxLayout(self._loading_widget)
        loading_layout.setSpacing(20)
        loading_layout.setContentsMargins(0, 0, 0, 0)

        # Loading message
        self._loading_label = QLabel("Please wait...")
        self._loading_label.setObjectName("LoadingDialog_LoadingLabel")
        self._loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        loading_layout.addWidget(self._loading_label)

        # Spinner
        self._spinner = LoadingSpinner(self)
        self._spinner.setObjectName("LoadingDialog_Spinner")
        loading_layout.addWidget(self._spinner, alignment=Qt.AlignmentFlag.AlignCenter)

        container_layout.addWidget(self._loading_widget)

        # Progress section
        self._progress_widget = QWidget()
        self._progress_widget.setObjectName("LoadingDialog_ProgressSection")
        self._progress_widget.setVisible(False)
        progress_layout = QVBoxLayout(self._progress_widget)
        progress_layout.setContentsMargins(0, 0, 0, 0)
        progress_layout.setSpacing(8)

        self._progress_label = QLabel()
        self._progress_label.setObjectName("LoadingDialog_ProgressLabel")
        self._progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self._progress_label.setWordWrap(True)
        progress_layout.addWidget(self._progress_label)

        self._progress_bar = QProgressBar()
        self._progress_bar.setObjectName("LoadingDialog_ProgressBar")
        self._progress_bar.setMinimum(0)
        self._progress_bar.setMaximum(100)
        self._progress_bar.setVisible(False)
        progress_layout.addWidget(self._progress_bar)

        container_layout.addWidget(self._progress_widget)

        # Fail-safe button
        self._fail_safe_button = QPushButton("Close Dialog")
        self._fail_safe_button.setObjectName("LoadingDialog_FailSafeButton")
        self._fail_safe_button.setVisible(False)
        self._fail_safe_button.clicked.connect(self._on_fail_safe_clicked)
        container_layout.addWidget(self._fail_safe_button)

        # Set up main dialog layout
        main_layout = QVBoxLayout(self)
        main_layout.setContentsMargins(
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
        )
        main_layout.addWidget(self._container)

    def _setup_interception(self) -> None:
        """Set up message box interception."""
        self._interceptor = LoadingDialogInterceptor(self)

    def _setup_timers(self) -> None:
        """Set up fail-safe timer."""
        self._fail_safe_timer = QTimer(self)
        self._fail_safe_timer.setSingleShot(True)
        self._fail_safe_timer.timeout.connect(self._show_fail_safe_button)

    # =========================================================================
    # CONTEXT MANAGER
    # =========================================================================

    def __enter__(self) -> "LoadingDialog":
        """Enter context manager."""
        self._context_manager_active = True
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Exit context manager with cleanup."""
        self._context_manager_active = False
        try:
            self.hide_loading()
        except RuntimeError:
            pass

    @contextmanager
    def loading_context(self, message: str = "Please wait..."):
        """
        Context manager for showing loading dialog.

        Args:
            message: Loading message to display.

        Yields:
            Self for progress updates.
        """
        self.show_loading(message)
        try:
            yield self
        finally:
            self.hide_loading()

    # =========================================================================
    # LOADING CONTROL
    # =========================================================================

    @Slot(str)
    def show_loading(self, message: str = "Please wait...") -> None:
        """
        Show the loading dialog.

        Args:
            message: Loading message to display.
        """
        # Reset state
        self._loading_label.setText(message)
        self._loading_widget.setVisible(True)
        self._progress_widget.setVisible(False)
        self._fail_safe_button.setVisible(False)
        self._current_subprocess = ""
        self._current_progress = -1

        # Start components
        self._interceptor.start()
        self._spinner.start()
        self._fail_safe_timer.start(30_000)  # 30 second timeout

        # Show and center on parent
        self.show()
        self.raise_()
        self.activateWindow()
        self._center_on_parent()

        QApplication.processEvents()

    @Slot()
    def hide_loading(self) -> None:
        """Hide the loading dialog."""
        # Stop components
        self._interceptor.stop()
        self._spinner.stop()
        self._fail_safe_timer.stop()

        # Clear state
        self._progress_widget.setVisible(False)
        self._current_subprocess = ""
        self._current_progress = -1

        self.close()

    @Slot()
    def force_close(self) -> None:
        """Force close the dialog."""
        self._interceptor.stop()
        self._spinner.stop()
        self._fail_safe_timer.stop()
        self.close()

    # =========================================================================
    # PROGRESS TRACKING
    # =========================================================================

    @Slot(str)
    def set_subprocess(self, name: str) -> None:
        """
        Set the current subprocess name.

        Args:
            name: Subprocess name to display.
        """
        self._current_subprocess = name
        self.subprocess_changed.emit(name)
        self._update_progress_display()

    @Slot(str, int)
    def update_progress(self, message: str, percentage: int = -1) -> None:
        """
        Update progress with message and optional percentage.

        Args:
            message: Progress message.
            percentage: Progress percentage (0-100), or -1 for indeterminate.
        """
        self._current_progress = percentage
        self.progress_updated.emit(message, percentage)

        # Active progress means the operation is alive -- reset fail-safe
        # and hide the button if it was already shown
        if self._fail_safe_timer.isActive():
            self._fail_safe_timer.start()
        if self._fail_safe_button.isVisible():
            self._fail_safe_button.setVisible(False)

        # Show progress widget and resize dialog to fit
        if not self._progress_widget.isVisible():
            self._progress_widget.setVisible(True)
            self.adjustSize()
            self._center_on_parent()

        # Update label
        if self._current_subprocess:
            full_message = f"{self._current_subprocess}: {message}"
        else:
            full_message = message
        self._progress_label.setText(full_message)

        # Update progress bar
        if percentage >= 0:
            self._progress_bar.setVisible(True)
            self._progress_bar.setValue(percentage)
        else:
            self._progress_bar.setVisible(False)

        QApplication.processEvents()

    @Slot(str)
    def update_message(self, message: str) -> None:
        """
        Update the main loading message.

        Args:
            message: New loading message.
        """
        self._loading_label.setText(message)
        QApplication.processEvents()

    def _update_progress_display(self) -> None:
        """Update progress label with subprocess name."""
        if self._progress_label.text() and self._current_subprocess:
            current = self._progress_label.text()
            if ":" in current:
                current = current.split(":", 1)[1].strip()
            self._progress_label.setText(f"{self._current_subprocess}: {current}")

    # =========================================================================
    # CONTROLLED DIALOG METHODS
    # =========================================================================

    def show_info(self, title: str, message: str) -> None:
        """
        Show an info message during loading.

        Temporarily hides the loading dialog, shows the message,
        then restores the loading dialog.

        Args:
            title: Message title.
            message: Message text.

        Example:
            with LoadingDialog(parent) as loading:
                loading.show_loading("Processing...")
                # ... do work ...
                loading.show_info("Step Complete", "First phase finished.")
        """
        self._show_controlled_message(title, message, "info")

    def show_warning(self, title: str, message: str) -> None:
        """
        Show a warning message during loading.

        Args:
            title: Message title.
            message: Warning message text.
        """
        self._show_controlled_message(title, message, "warning")

    def show_error(self, title: str, message: str) -> None:
        """
        Show an error message during loading.

        Args:
            title: Message title.
            message: Error message text.
        """
        self._show_controlled_message(title, message, "error")

    def ask_question(self, title: str, message: str) -> bool:
        """
        Ask a yes/no question during loading.

        Temporarily hides the loading dialog, shows the question,
        then restores the loading dialog.

        Args:
            title: Question title.
            message: Question text.

        Returns:
            True if user clicked Yes, False otherwise.

        Example:
            with LoadingDialog(parent) as loading:
                loading.show_loading("Processing...")
                if loading.ask_question("Continue?", "Found issues. Proceed anyway?"):
                    # User clicked Yes
                    continue_processing()
        """
        from PySide6.QtWidgets import QMessageBox
        from src.shared_services.prompt_dialogs.message_box.message_dialog import (
            MessageDialog,
            MessageType,
        )

        # Temporarily hide loading
        self.hide()

        try:
            dialog = MessageDialog(
                parent=self.parent(),
                message_type=MessageType.QUESTION,
                title=title,
                message=message,
                buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            )
            dialog.exec()
            result = dialog.get_result() == QMessageBox.StandardButton.Yes
        finally:
            # Restore loading dialog
            self.show()
            self.raise_()
            self.activateWindow()

        return result

    def get_input(
        self,
        title: str,
        prompt: str,
        initial_text: str = "",
        blacklist: Optional[List[str]] = None,
    ) -> Tuple[str, bool]:
        """
        Get text input from the user during loading.

        Temporarily hides the loading dialog, shows the input dialog,
        then restores the loading dialog.

        Args:
            title: Dialog title.
            prompt: Input prompt text.
            initial_text: Initial text in the input field.
            blacklist: List of disallowed values.

        Returns:
            Tuple of (entered_text, accepted). Text is empty if not accepted.

        Example:
            with LoadingDialog(parent) as loading:
                loading.show_loading("Creating project...")
                # ... do initial work ...
                name, ok = loading.get_input(
                    "Project Name",
                    "Enter a name for the new project:",
                    initial_text="New Project"
                )
                if ok:
                    finalize_project(name)
        """
        from src.shared_services.prompt_dialogs.input.input_dialog import InputDialog

        # Temporarily hide loading
        self.hide()

        try:
            dialog = InputDialog(
                parent=self.parent(),
                title=title,
                prompt=prompt,
                initial_text=initial_text,
                blacklist=blacklist,
            )
            accepted = dialog.exec() == InputDialog.DialogCode.Accepted
            text = dialog.get_entered_text() if accepted else ""
        finally:
            # Restore loading dialog
            self.show()
            self.raise_()
            self.activateWindow()

        return text, accepted

    def _show_controlled_message(
        self,
        title: str,
        message: str,
        message_type: str,
    ) -> None:
        """
        Internal method to show a controlled message dialog.

        Args:
            title: Message title.
            message: Message text.
            message_type: One of "info", "warning", "error".
        """
        from PySide6.QtWidgets import QMessageBox
        from src.shared_services.prompt_dialogs.message_box.message_dialog import (
            MessageDialog,
            MessageType,
        )

        type_map = {
            "info": MessageType.INFO,
            "warning": MessageType.WARNING,
            "error": MessageType.ERROR,
        }
        msg_type = type_map.get(message_type, MessageType.INFO)

        # Temporarily hide loading
        self.hide()

        try:
            dialog = MessageDialog(
                parent=self.parent(),
                message_type=msg_type,
                title=title,
                message=message,
                buttons=QMessageBox.StandardButton.Ok,
            )
            dialog.exec()
        finally:
            # Restore loading dialog
            self.show()
            self.raise_()
            self.activateWindow()

    # =========================================================================
    # FAIL-SAFE
    # =========================================================================

    @Slot()
    def _show_fail_safe_button(self) -> None:
        """Show fail-safe button after timeout."""
        self._fail_safe_button.setVisible(True)
        self._loading_label.setText("Operation is taking longer than expected...")
        self.adjustSize()
        self._center_on_parent()

    @Slot()
    def _on_fail_safe_clicked(self) -> None:
        """Handle fail-safe button click."""
        self.cancelled.emit()
        self.force_close()

    # =========================================================================
    # UTILITIES
    # =========================================================================

    @staticmethod
    def _find_top_level_window(widget: Optional[QWidget]) -> Optional[QWidget]:
        """
        Find the top-level window (main window) from any widget.

        Traverses up the widget hierarchy to find a stable parent that won't
        be destroyed during view transitions.

        Args:
            widget: Starting widget to search from.

        Returns:
            The top-level window, or None if not found.
        """
        if widget is None:
            return None

        try:
            # Traverse up to find the top-level window
            current = widget
            while current is not None:
                # Check if this is a top-level window
                if current.isWindow():
                    return current
                current = current.parent()

            # Fallback: use the widget's window() method
            return widget.window()
        except RuntimeError:
            return None

    def _center_on_parent(self) -> None:
        """Center the dialog on parent widget or screen."""
        try:
            parent = self.parent()
            if parent is not None:
                # Center on parent widget
                parent_rect = parent.geometry()
                # Get parent's global position
                if hasattr(parent, 'mapToGlobal'):
                    parent_pos = parent.mapToGlobal(parent.rect().topLeft())
                    x = parent_pos.x() + (parent_rect.width() - self.width()) // 2
                    y = parent_pos.y() + (parent_rect.height() - self.height()) // 2
                    self.move(x, y)
                    return

            # Fallback: center on primary screen
            screen = QApplication.primaryScreen()
            if screen:
                geometry = screen.geometry()
                x = (geometry.width() - self.width()) // 2
                y = (geometry.height() - self.height()) // 2
                self.move(x, y)
        except RuntimeError:
            pass

    @Slot(str)
    def _on_theme_changed(self, theme: str) -> None:
        """Handle theme change."""
        # Update spinner color
        try:
            palette = QApplication.instance().palette()
            highlight = palette.color(QPalette.ColorRole.Highlight)
            self._spinner.set_color(highlight)
        except (RuntimeError, AttributeError):
            pass

    def closeEvent(self, event: QCloseEvent) -> None:
        """Clean up on close."""
        self._interceptor.stop()
        self._spinner.stop()
        self._fail_safe_timer.stop()
        super().closeEvent(event)
__enter__()

Enter context manager.

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def __enter__(self) -> "LoadingDialog":
    """Enter context manager."""
    self._context_manager_active = True
    return self
__exit__(exc_type, exc_val, exc_tb)

Exit context manager with cleanup.

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
    """Exit context manager with cleanup."""
    self._context_manager_active = False
    try:
        self.hide_loading()
    except RuntimeError:
        pass
__init__(parent=None)

Initialize the loading dialog.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget. The dialog will automatically use the top-level window (main window) as its actual parent to ensure stability when views are being loaded/replaced.

None
Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def __init__(self, parent: Optional[QWidget] = None) -> None:
    """
    Initialize the loading dialog.

    Args:
        parent: Parent widget. The dialog will automatically use the
            top-level window (main window) as its actual parent to ensure
            stability when views are being loaded/replaced.
    """
    # Find the top-level window to use as stable parent
    # This prevents issues when the original parent gets replaced during loading
    stable_parent = self._find_top_level_window(parent)

    super().__init__(
        parent=stable_parent,
        stylesheets=[DialogStylesheets.Main, DialogStylesheets.LoadingDialog],
    )

    # Store original parent for centering purposes
    self._centering_widget = parent

    # State
    self._current_subprocess = ""
    self._current_progress = -1
    self._context_manager_active = False

    # Setup
    self._setup_ui()
    self._setup_interception()
    self._setup_timers()

    # Register with dialog registry
    DialogRegistry.instance().register(self, "loading")
ask_question(title, message)

Ask a yes/no question during loading.

Temporarily hides the loading dialog, shows the question, then restores the loading dialog.

Parameters:

Name Type Description Default
title str

Question title.

required
message str

Question text.

required

Returns:

Type Description
bool

True if user clicked Yes, False otherwise.

Example

with LoadingDialog(parent) as loading: loading.show_loading("Processing...") if loading.ask_question("Continue?", "Found issues. Proceed anyway?"): # User clicked Yes continue_processing()

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def ask_question(self, title: str, message: str) -> bool:
    """
    Ask a yes/no question during loading.

    Temporarily hides the loading dialog, shows the question,
    then restores the loading dialog.

    Args:
        title: Question title.
        message: Question text.

    Returns:
        True if user clicked Yes, False otherwise.

    Example:
        with LoadingDialog(parent) as loading:
            loading.show_loading("Processing...")
            if loading.ask_question("Continue?", "Found issues. Proceed anyway?"):
                # User clicked Yes
                continue_processing()
    """
    from PySide6.QtWidgets import QMessageBox
    from src.shared_services.prompt_dialogs.message_box.message_dialog import (
        MessageDialog,
        MessageType,
    )

    # Temporarily hide loading
    self.hide()

    try:
        dialog = MessageDialog(
            parent=self.parent(),
            message_type=MessageType.QUESTION,
            title=title,
            message=message,
            buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
        )
        dialog.exec()
        result = dialog.get_result() == QMessageBox.StandardButton.Yes
    finally:
        # Restore loading dialog
        self.show()
        self.raise_()
        self.activateWindow()

    return result
closeEvent(event)

Clean up on close.

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def closeEvent(self, event: QCloseEvent) -> None:
    """Clean up on close."""
    self._interceptor.stop()
    self._spinner.stop()
    self._fail_safe_timer.stop()
    super().closeEvent(event)
force_close()

Force close the dialog.

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
@Slot()
def force_close(self) -> None:
    """Force close the dialog."""
    self._interceptor.stop()
    self._spinner.stop()
    self._fail_safe_timer.stop()
    self.close()
get_input(title, prompt, initial_text='', blacklist=None)

Get text input from the user during loading.

Temporarily hides the loading dialog, shows the input dialog, then restores the loading dialog.

Parameters:

Name Type Description Default
title str

Dialog title.

required
prompt str

Input prompt text.

required
initial_text str

Initial text in the input field.

''
blacklist Optional[List[str]]

List of disallowed values.

None

Returns:

Type Description
Tuple[str, bool]

Tuple of (entered_text, accepted). Text is empty if not accepted.

Example

with LoadingDialog(parent) as loading: loading.show_loading("Creating project...") # ... do initial work ... name, ok = loading.get_input( "Project Name", "Enter a name for the new project:", initial_text="New Project" ) if ok: finalize_project(name)

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def get_input(
    self,
    title: str,
    prompt: str,
    initial_text: str = "",
    blacklist: Optional[List[str]] = None,
) -> Tuple[str, bool]:
    """
    Get text input from the user during loading.

    Temporarily hides the loading dialog, shows the input dialog,
    then restores the loading dialog.

    Args:
        title: Dialog title.
        prompt: Input prompt text.
        initial_text: Initial text in the input field.
        blacklist: List of disallowed values.

    Returns:
        Tuple of (entered_text, accepted). Text is empty if not accepted.

    Example:
        with LoadingDialog(parent) as loading:
            loading.show_loading("Creating project...")
            # ... do initial work ...
            name, ok = loading.get_input(
                "Project Name",
                "Enter a name for the new project:",
                initial_text="New Project"
            )
            if ok:
                finalize_project(name)
    """
    from src.shared_services.prompt_dialogs.input.input_dialog import InputDialog

    # Temporarily hide loading
    self.hide()

    try:
        dialog = InputDialog(
            parent=self.parent(),
            title=title,
            prompt=prompt,
            initial_text=initial_text,
            blacklist=blacklist,
        )
        accepted = dialog.exec() == InputDialog.DialogCode.Accepted
        text = dialog.get_entered_text() if accepted else ""
    finally:
        # Restore loading dialog
        self.show()
        self.raise_()
        self.activateWindow()

    return text, accepted
hide_loading()

Hide the loading dialog.

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
@Slot()
def hide_loading(self) -> None:
    """Hide the loading dialog."""
    # Stop components
    self._interceptor.stop()
    self._spinner.stop()
    self._fail_safe_timer.stop()

    # Clear state
    self._progress_widget.setVisible(False)
    self._current_subprocess = ""
    self._current_progress = -1

    self.close()
loading_context(message='Please wait...')

Context manager for showing loading dialog.

Parameters:

Name Type Description Default
message str

Loading message to display.

'Please wait...'

Yields:

Type Description

Self for progress updates.

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
@contextmanager
def loading_context(self, message: str = "Please wait..."):
    """
    Context manager for showing loading dialog.

    Args:
        message: Loading message to display.

    Yields:
        Self for progress updates.
    """
    self.show_loading(message)
    try:
        yield self
    finally:
        self.hide_loading()
set_subprocess(name)

Set the current subprocess name.

Parameters:

Name Type Description Default
name str

Subprocess name to display.

required
Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
@Slot(str)
def set_subprocess(self, name: str) -> None:
    """
    Set the current subprocess name.

    Args:
        name: Subprocess name to display.
    """
    self._current_subprocess = name
    self.subprocess_changed.emit(name)
    self._update_progress_display()
show_error(title, message)

Show an error message during loading.

Parameters:

Name Type Description Default
title str

Message title.

required
message str

Error message text.

required
Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def show_error(self, title: str, message: str) -> None:
    """
    Show an error message during loading.

    Args:
        title: Message title.
        message: Error message text.
    """
    self._show_controlled_message(title, message, "error")
show_info(title, message)

Show an info message during loading.

Temporarily hides the loading dialog, shows the message, then restores the loading dialog.

Parameters:

Name Type Description Default
title str

Message title.

required
message str

Message text.

required
Example

with LoadingDialog(parent) as loading: loading.show_loading("Processing...") # ... do work ... loading.show_info("Step Complete", "First phase finished.")

Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def show_info(self, title: str, message: str) -> None:
    """
    Show an info message during loading.

    Temporarily hides the loading dialog, shows the message,
    then restores the loading dialog.

    Args:
        title: Message title.
        message: Message text.

    Example:
        with LoadingDialog(parent) as loading:
            loading.show_loading("Processing...")
            # ... do work ...
            loading.show_info("Step Complete", "First phase finished.")
    """
    self._show_controlled_message(title, message, "info")
show_loading(message='Please wait...')

Show the loading dialog.

Parameters:

Name Type Description Default
message str

Loading message to display.

'Please wait...'
Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
@Slot(str)
def show_loading(self, message: str = "Please wait...") -> None:
    """
    Show the loading dialog.

    Args:
        message: Loading message to display.
    """
    # Reset state
    self._loading_label.setText(message)
    self._loading_widget.setVisible(True)
    self._progress_widget.setVisible(False)
    self._fail_safe_button.setVisible(False)
    self._current_subprocess = ""
    self._current_progress = -1

    # Start components
    self._interceptor.start()
    self._spinner.start()
    self._fail_safe_timer.start(30_000)  # 30 second timeout

    # Show and center on parent
    self.show()
    self.raise_()
    self.activateWindow()
    self._center_on_parent()

    QApplication.processEvents()
show_warning(title, message)

Show a warning message during loading.

Parameters:

Name Type Description Default
title str

Message title.

required
message str

Warning message text.

required
Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
def show_warning(self, title: str, message: str) -> None:
    """
    Show a warning message during loading.

    Args:
        title: Message title.
        message: Warning message text.
    """
    self._show_controlled_message(title, message, "warning")
update_message(message)

Update the main loading message.

Parameters:

Name Type Description Default
message str

New loading message.

required
Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
@Slot(str)
def update_message(self, message: str) -> None:
    """
    Update the main loading message.

    Args:
        message: New loading message.
    """
    self._loading_label.setText(message)
    QApplication.processEvents()
update_progress(message, percentage=-1)

Update progress with message and optional percentage.

Parameters:

Name Type Description Default
message str

Progress message.

required
percentage int

Progress percentage (0-100), or -1 for indeterminate.

-1
Source code in src\shared_services\prompt_dialogs\loading\loading_dialog.py
@Slot(str, int)
def update_progress(self, message: str, percentage: int = -1) -> None:
    """
    Update progress with message and optional percentage.

    Args:
        message: Progress message.
        percentage: Progress percentage (0-100), or -1 for indeterminate.
    """
    self._current_progress = percentage
    self.progress_updated.emit(message, percentage)

    # Active progress means the operation is alive -- reset fail-safe
    # and hide the button if it was already shown
    if self._fail_safe_timer.isActive():
        self._fail_safe_timer.start()
    if self._fail_safe_button.isVisible():
        self._fail_safe_button.setVisible(False)

    # Show progress widget and resize dialog to fit
    if not self._progress_widget.isVisible():
        self._progress_widget.setVisible(True)
        self.adjustSize()
        self._center_on_parent()

    # Update label
    if self._current_subprocess:
        full_message = f"{self._current_subprocess}: {message}"
    else:
        full_message = message
    self._progress_label.setText(full_message)

    # Update progress bar
    if percentage >= 0:
        self._progress_bar.setVisible(True)
        self._progress_bar.setValue(percentage)
    else:
        self._progress_bar.setVisible(False)

    QApplication.processEvents()

LoadingDialogInterceptor

Bases: InterceptionManager

Interception manager specifically for LoadingDialog integration.

Wraps InterceptionManager with weak reference to a LoadingDialog for automatic callback routing. When a message is intercepted during loading, the loading dialog is hidden and a proper MessageDialog is shown instead, ensuring the user sees identical dialogs whether intercepted or not.

Example

Using with LoadingDialog::

interceptor = LoadingDialogInterceptor(loading_dialog)
interceptor.start()
# ... loading work ...
interceptor.stop()
Source code in src\shared_services\prompt_dialogs\interception\interceptor.py
class LoadingDialogInterceptor(InterceptionManager):
    """
    Interception manager specifically for LoadingDialog integration.

    Wraps InterceptionManager with weak reference to a LoadingDialog for
    automatic callback routing. When a message is intercepted during loading,
    the loading dialog is hidden and a proper MessageDialog is shown instead,
    ensuring the user sees identical dialogs whether intercepted or not.

    Example:
        Using with LoadingDialog::

            interceptor = LoadingDialogInterceptor(loading_dialog)
            interceptor.start()
            # ... loading work ...
            interceptor.stop()
    """

    def __init__(
        self,
        loading_dialog: "LoadingDialog",
        parent: Optional[QObject] = None,
    ) -> None:
        """
        Initialize the loading dialog interceptor.

        Args:
            loading_dialog: The loading dialog to route messages to.
            parent: Parent QObject.
        """
        super().__init__(parent=parent)

        self._dialog_ref: weakref.ref = weakref.ref(loading_dialog)
        self.set_callback(self._route_to_dialog)

    def _route_to_dialog(
        self,
        title: str,
        text: str,
        icon: QMessageBox.Icon,
        buttons: QMessageBox.StandardButton,
    ) -> QMessageBox.StandardButton:
        """
        Route intercepted message by hiding loading and showing MessageDialog.

        This approach ensures intercepted messages look identical to normal
        messages - the user cannot tell the difference.

        Args:
            title: Message title.
            text: Message text.
            icon: Message icon type.
            buttons: Standard buttons.

        Returns:
            The selected button result.
        """
        dialog = self._dialog_ref()
        if dialog is None:
            return QMessageBox.StandardButton.Ok

        try:
            # Check if dialog is still visible
            if not dialog.isVisible():
                return QMessageBox.StandardButton.Ok

            # Get parent for the message dialog
            parent_widget = dialog.parent()

            # Temporarily hide loading dialog
            dialog.hide()

            # Import here to avoid circular imports
            from src.shared_services.prompt_dialogs.message_box.message_dialog import (
                MessageDialog,
                MessageType,
            )

            # Map QMessageBox.Icon to MessageType
            icon_to_type = {
                QMessageBox.Icon.Information: MessageType.INFO,
                QMessageBox.Icon.Warning: MessageType.WARNING,
                QMessageBox.Icon.Critical: MessageType.ERROR,
                QMessageBox.Icon.Question: MessageType.QUESTION,
            }
            message_type = icon_to_type.get(icon, MessageType.INFO)

            # Show proper MessageDialog (identical to normal messages)
            msg_dialog = MessageDialog(
                parent=parent_widget,
                message_type=message_type,
                title=title,
                message=text,
                buttons=buttons,
            )
            msg_dialog.exec()
            result = msg_dialog.get_result()

            # Restore loading dialog
            dialog.show()
            dialog.raise_()
            dialog.activateWindow()

            return result

        except RuntimeError:
            # Dialog was deleted
            pass

        return QMessageBox.StandardButton.Ok

    def _intercept_getText(
        self,
        parent,
        title: str,
        label: str,
        echo: QLineEdit.EchoMode = QLineEdit.EchoMode.Normal,
        text: str = "",
        *args,
        **kwargs,
    ) -> Tuple[str, bool]:
        """
        Intercept QInputDialog.getText() by hiding loading and showing InputDialog.

        Returns:
            Tuple of (entered_text, accepted).
        """
        dialog = self._dialog_ref()
        if dialog is None or not dialog.isVisible():
            # Fall back to parent implementation
            return super()._intercept_getText(parent, title, label, echo, text, *args, **kwargs)

        try:
            # Get parent for the input dialog
            parent_widget = dialog.parent()

            # Temporarily hide loading dialog
            dialog.hide()

            # Import here to avoid circular imports
            from src.shared_services.prompt_dialogs.input.input_dialog import InputDialog

            # Show our styled InputDialog
            input_dialog = InputDialog(
                parent=parent_widget,
                title=title,
                prompt=label,
                initial_text=text,
            )
            accepted = input_dialog.exec() == InputDialog.DialogCode.Accepted
            result_text = input_dialog.get_entered_text() if accepted else ""

            # Restore loading dialog
            dialog.show()
            dialog.raise_()
            dialog.activateWindow()

            return result_text, accepted

        except RuntimeError:
            pass

        return "", False

    def _intercept_getItem(
        self,
        parent,
        title: str,
        label: str,
        items,
        current: int = 0,
        editable: bool = True,
        *args,
        **kwargs,
    ) -> Tuple[str, bool]:
        """
        Intercept QInputDialog.getItem() by hiding loading and showing dialog.

        For now, falls back to the default Qt dialog since we don't have
        a custom item selection dialog yet.

        Returns:
            Tuple of (selected_item, accepted).
        """
        dialog = self._dialog_ref()
        if dialog is None or not dialog.isVisible():
            return super()._intercept_getItem(parent, title, label, items, current, editable, *args, **kwargs)

        try:
            parent_widget = dialog.parent()
            dialog.hide()

            # Use original QInputDialog for item selection (we don't have a styled version yet)
            if self._original_getItem is not None:
                result = self._original_getItem(parent_widget, title, label, items, current, editable, *args, **kwargs)
            else:
                result = ("", False)

            dialog.show()
            dialog.raise_()
            dialog.activateWindow()

            return result

        except RuntimeError:
            pass

        return "", False

    def _intercept_getInt(
        self,
        parent,
        title: str,
        label: str,
        value: int = 0,
        minValue: int = -2147483647,
        maxValue: int = 2147483647,
        step: int = 1,
        *args,
        **kwargs,
    ) -> Tuple[int, bool]:
        """
        Intercept QInputDialog.getInt() by hiding loading and showing dialog.

        For now, falls back to the default Qt dialog since we don't have
        a custom integer input dialog yet.

        Returns:
            Tuple of (entered_value, accepted).
        """
        dialog = self._dialog_ref()
        if dialog is None or not dialog.isVisible():
            return super()._intercept_getInt(parent, title, label, value, minValue, maxValue, step, *args, **kwargs)

        try:
            parent_widget = dialog.parent()
            dialog.hide()

            # Use original QInputDialog for int input (we don't have a styled version yet)
            if self._original_getInt is not None:
                result = self._original_getInt(parent_widget, title, label, value, minValue, maxValue, step, *args, **kwargs)
            else:
                result = (0, False)

            dialog.show()
            dialog.raise_()
            dialog.activateWindow()

            return result

        except RuntimeError:
            pass

        return 0, False
__init__(loading_dialog, parent=None)

Initialize the loading dialog interceptor.

Parameters:

Name Type Description Default
loading_dialog LoadingDialog

The loading dialog to route messages to.

required
parent Optional[QObject]

Parent QObject.

None
Source code in src\shared_services\prompt_dialogs\interception\interceptor.py
def __init__(
    self,
    loading_dialog: "LoadingDialog",
    parent: Optional[QObject] = None,
) -> None:
    """
    Initialize the loading dialog interceptor.

    Args:
        loading_dialog: The loading dialog to route messages to.
        parent: Parent QObject.
    """
    super().__init__(parent=parent)

    self._dialog_ref: weakref.ref = weakref.ref(loading_dialog)
    self.set_callback(self._route_to_dialog)

LoadingSpinner

Bases: QWidget

Animated loading spinner widget.

Displays a circular spinner animation with customizable colors and animation speed. Integrates with the application theme system.

Signals

started: Emitted when animation starts. stopped: Emitted when animation stops.

Example

Using the spinner::

spinner = LoadingSpinner(parent)
spinner.start()
# ... loading work ...
spinner.stop()
Source code in src\shared_services\prompt_dialogs\loading\loading_spinner.py
class LoadingSpinner(QWidget):
    """
    Animated loading spinner widget.

    Displays a circular spinner animation with customizable colors
    and animation speed. Integrates with the application theme system.

    Signals:
        started: Emitted when animation starts.
        stopped: Emitted when animation stops.

    Example:
        Using the spinner::

            spinner = LoadingSpinner(parent)
            spinner.start()
            # ... loading work ...
            spinner.stop()
    """

    started: Signal = Signal()
    stopped: Signal = Signal()

    def __init__(
        self,
        parent: Optional[QWidget] = None,
        size: int = 50,
        color: Optional[QColor] = None,
    ) -> None:
        """
        Initialize the loading spinner.

        Args:
            parent: Parent widget.
            size: Widget size in pixels.
            color: Primary spinner color (uses theme highlight if not set).
        """
        super().__init__(parent)

        self._angle = 0
        self._is_running = False
        self._timer = QTimer(self)
        self._timer.timeout.connect(self._rotate)

        # Spinner appearance
        self._primary_color = color or QColor(25, 118, 210)
        self._lines_count = 8
        self._line_length = 10
        self._line_width = 3
        self._inner_radius = 12

        # Configure widget
        self.setObjectName("LoadingSpinner")
        self.setFixedSize(size, size)

    def set_color(self, color: QColor) -> None:
        """
        Set the spinner primary color.

        Args:
            color: New primary color.
        """
        self._primary_color = color
        self.update()

    def set_lines(
        self,
        count: int = 8,
        length: int = 10,
        width: int = 3,
        inner_radius: int = 12,
    ) -> None:
        """
        Configure spinner line appearance.

        Args:
            count: Number of lines in the spinner.
            length: Length of each line.
            width: Width of each line.
            inner_radius: Distance from center to line start.
        """
        self._lines_count = count
        self._line_length = length
        self._line_width = width
        self._inner_radius = inner_radius
        self.update()

    def is_running(self) -> bool:
        """
        Check if the spinner is currently animating.

        Returns:
            True if the animation is running.
        """
        return self._is_running

    @Slot()
    def start(self, interval_ms: int = 80) -> None:
        """
        Start the spinner animation.

        Args:
            interval_ms: Animation interval in milliseconds.
        """
        if self._is_running:
            return

        self._is_running = True
        self._timer.start(interval_ms)
        self.started.emit()

    @Slot()
    def stop(self) -> None:
        """Stop the spinner animation."""
        if not self._is_running:
            return

        self._is_running = False
        self._timer.stop()
        self.stopped.emit()

    @Slot()
    def _rotate(self) -> None:
        """Rotate the spinner by one step."""
        self._angle = (self._angle + 45) % 360
        self.update()

    def paintEvent(self, event) -> None:
        """Paint the spinner."""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # Move to center and apply rotation
        painter.translate(self.width() / 2, self.height() / 2)
        painter.rotate(self._angle)

        # Draw each line with fading alpha
        for i in range(self._lines_count):
            alpha = 1.0 - (i / self._lines_count) * 0.8
            color = QColor(self._primary_color)
            color.setAlphaF(alpha)

            pen = QPen(color)
            pen.setWidth(self._line_width)
            pen.setCapStyle(Qt.PenCapStyle.RoundCap)
            painter.setPen(pen)

            painter.drawLine(
                self._inner_radius,
                0,
                self._inner_radius + self._line_length,
                0,
            )
            painter.rotate(360 / self._lines_count)
__init__(parent=None, size=50, color=None)

Initialize the loading spinner.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget.

None
size int

Widget size in pixels.

50
color Optional[QColor]

Primary spinner color (uses theme highlight if not set).

None
Source code in src\shared_services\prompt_dialogs\loading\loading_spinner.py
def __init__(
    self,
    parent: Optional[QWidget] = None,
    size: int = 50,
    color: Optional[QColor] = None,
) -> None:
    """
    Initialize the loading spinner.

    Args:
        parent: Parent widget.
        size: Widget size in pixels.
        color: Primary spinner color (uses theme highlight if not set).
    """
    super().__init__(parent)

    self._angle = 0
    self._is_running = False
    self._timer = QTimer(self)
    self._timer.timeout.connect(self._rotate)

    # Spinner appearance
    self._primary_color = color or QColor(25, 118, 210)
    self._lines_count = 8
    self._line_length = 10
    self._line_width = 3
    self._inner_radius = 12

    # Configure widget
    self.setObjectName("LoadingSpinner")
    self.setFixedSize(size, size)
is_running()

Check if the spinner is currently animating.

Returns:

Type Description
bool

True if the animation is running.

Source code in src\shared_services\prompt_dialogs\loading\loading_spinner.py
def is_running(self) -> bool:
    """
    Check if the spinner is currently animating.

    Returns:
        True if the animation is running.
    """
    return self._is_running
paintEvent(event)

Paint the spinner.

Source code in src\shared_services\prompt_dialogs\loading\loading_spinner.py
def paintEvent(self, event) -> None:
    """Paint the spinner."""
    painter = QPainter(self)
    painter.setRenderHint(QPainter.RenderHint.Antialiasing)

    # Move to center and apply rotation
    painter.translate(self.width() / 2, self.height() / 2)
    painter.rotate(self._angle)

    # Draw each line with fading alpha
    for i in range(self._lines_count):
        alpha = 1.0 - (i / self._lines_count) * 0.8
        color = QColor(self._primary_color)
        color.setAlphaF(alpha)

        pen = QPen(color)
        pen.setWidth(self._line_width)
        pen.setCapStyle(Qt.PenCapStyle.RoundCap)
        painter.setPen(pen)

        painter.drawLine(
            self._inner_radius,
            0,
            self._inner_radius + self._line_length,
            0,
        )
        painter.rotate(360 / self._lines_count)
set_color(color)

Set the spinner primary color.

Parameters:

Name Type Description Default
color QColor

New primary color.

required
Source code in src\shared_services\prompt_dialogs\loading\loading_spinner.py
def set_color(self, color: QColor) -> None:
    """
    Set the spinner primary color.

    Args:
        color: New primary color.
    """
    self._primary_color = color
    self.update()
set_lines(count=8, length=10, width=3, inner_radius=12)

Configure spinner line appearance.

Parameters:

Name Type Description Default
count int

Number of lines in the spinner.

8
length int

Length of each line.

10
width int

Width of each line.

3
inner_radius int

Distance from center to line start.

12
Source code in src\shared_services\prompt_dialogs\loading\loading_spinner.py
def set_lines(
    self,
    count: int = 8,
    length: int = 10,
    width: int = 3,
    inner_radius: int = 12,
) -> None:
    """
    Configure spinner line appearance.

    Args:
        count: Number of lines in the spinner.
        length: Length of each line.
        width: Width of each line.
        inner_radius: Distance from center to line start.
    """
    self._lines_count = count
    self._line_length = length
    self._line_width = width
    self._inner_radius = inner_radius
    self.update()
start(interval_ms=80)

Start the spinner animation.

Parameters:

Name Type Description Default
interval_ms int

Animation interval in milliseconds.

80
Source code in src\shared_services\prompt_dialogs\loading\loading_spinner.py
@Slot()
def start(self, interval_ms: int = 80) -> None:
    """
    Start the spinner animation.

    Args:
        interval_ms: Animation interval in milliseconds.
    """
    if self._is_running:
        return

    self._is_running = True
    self._timer.start(interval_ms)
    self.started.emit()
stop()

Stop the spinner animation.

Source code in src\shared_services\prompt_dialogs\loading\loading_spinner.py
@Slot()
def stop(self) -> None:
    """Stop the spinner animation."""
    if not self._is_running:
        return

    self._is_running = False
    self._timer.stop()
    self.stopped.emit()

MessageBoxType

Bases: Enum

Type of message box that was intercepted.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
class MessageBoxType(Enum):
    """Type of message box that was intercepted."""

    INFORMATION = auto()
    WARNING = auto()
    CRITICAL = auto()
    QUESTION = auto()
    UNKNOWN = auto()

MessageDialog

Bases: ThemeAwareDialog

Theme-aware message dialog.

Provides a consistent, styled alternative to QMessageBox with automatic theme support.

Signals

button_clicked: Emitted when a button is clicked with the button flag.

Example

Creating a custom message dialog::

dialog = MessageDialog(
    parent=self,
    message_type=MessageType.WARNING,
    title="Confirm",
    message="Are you sure?",
    buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
result = dialog.exec()
Source code in src\shared_services\prompt_dialogs\message_box\message_dialog.py
class MessageDialog(ThemeAwareDialog):
    """
    Theme-aware message dialog.

    Provides a consistent, styled alternative to QMessageBox with
    automatic theme support.

    Signals:
        button_clicked: Emitted when a button is clicked with the button flag.

    Example:
        Creating a custom message dialog::

            dialog = MessageDialog(
                parent=self,
                message_type=MessageType.WARNING,
                title="Confirm",
                message="Are you sure?",
                buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            )
            result = dialog.exec()
    """

    button_clicked: Signal = Signal(int)

    def __init__(
        self,
        parent: QWidget | None = None,
        message_type: MessageType = MessageType.INFO,
        title: str = "",
        message: str = "",
        buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
    ) -> None:
        """
        Initialize the message dialog.

        Args:
            parent: Parent widget.
            message_type: Type of message (INFO, WARNING, ERROR, QUESTION).
            title: Dialog title.
            message: Message to display.
            buttons: Standard buttons to show.
        """
        super().__init__(
            parent=parent,
            stylesheets=[DialogStylesheets.Main, DialogStylesheets.MessageDialog],
        )

        self._message_type = message_type
        self._title = title or _DEFAULT_TITLES.get(message_type, "Message")
        self._message = message
        self._buttons = buttons
        self._result = QMessageBox.StandardButton.NoButton
        self._button_widgets: Dict[QMessageBox.StandardButton, QPushButton] = {}

        self._setup_ui()
        self._setup_icon()

    def _setup_ui(self) -> None:
        """Set up the dialog UI."""
        self.setObjectName("MessageDialog")
        self.setWindowTitle(self._title)
        self.setMinimumWidth(420 + 2 * self._SHADOW_MARGIN)
        self.setMaximumWidth(520 + 2 * self._SHADOW_MARGIN)

        # Set accent color based on message type
        from PySide6.QtGui import QColor
        accent_map = {
            MessageType.INFO: QColor(8, 145, 178),
            MessageType.WARNING: QColor(234, 88, 12),
            MessageType.ERROR: QColor(220, 38, 38),
            MessageType.QUESTION: QColor(37, 99, 235),
        }
        self._accent_color = accent_map.get(self._message_type, QColor(8, 145, 178))

        # Main dialog layout (transparent)
        dialog_layout = QVBoxLayout(self)
        dialog_layout.setContentsMargins(
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
        )

        # Container frame
        self._container = QFrame()
        self._container.setObjectName("MessageDialog_Container")

        # Container layout - compact
        container_layout = QVBoxLayout(self._container)
        container_layout.setSpacing(10)
        container_layout.setContentsMargins(14, 12, 14, 12)

        # Content area (icon + text)
        header_layout = QHBoxLayout()
        header_layout.setSpacing(10)

        # Icon
        self._icon_label = QLabel()
        self._icon_label.setObjectName("MessageDialog_Icon")
        self._icon_label.setFixedSize(28, 28)
        self._icon_label.setScaledContents(False)
        self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        header_layout.addWidget(self._icon_label, 0, Qt.AlignmentFlag.AlignTop)

        # Text area
        text_layout = QVBoxLayout()
        text_layout.setSpacing(4)

        # Title label
        self._title_label = QLabel(self._title)
        self._title_label.setObjectName("MessageDialog_Title")
        text_layout.addWidget(self._title_label)

        # Message label
        self._message_label = QLabel(self._message)
        self._message_label.setObjectName("MessageDialog_Message")
        self._message_label.setWordWrap(True)
        text_layout.addWidget(self._message_label)

        header_layout.addLayout(text_layout, 1)
        container_layout.addLayout(header_layout)

        # Button area
        button_layout = QHBoxLayout()
        button_layout.setSpacing(6)
        button_layout.addStretch()

        self._button_container = QWidget()
        self._button_container.setObjectName("MessageDialog_ButtonContainer")
        self._button_container.setLayout(button_layout)

        self._create_buttons(button_layout)

        container_layout.addWidget(self._button_container)

        dialog_layout.addWidget(self._container)

    def _setup_icon(self) -> None:
        """Set up the message type icon."""
        try:
            from src.shared_services.path_management.api import get_path_str
            from src.shared_services.rendering.icons.api import render_svg

            icon_map = {
                MessageType.INFO: Icons.Action.Info,
                MessageType.WARNING: Icons.Alert.Warning,
                MessageType.ERROR: Icons.Alert.Error,
                MessageType.QUESTION: Icons.Action.Help,
            }

            icon_path_def = icon_map.get(self._message_type, Icons.Action.Info)
            # Render with theme-appropriate color
            self._icon_label.setPixmap(render_svg(icon_path_def, size=24))

        except (ImportError, FileNotFoundError):
            # Fall back to standard icon
            self._set_standard_icon()

    def _set_standard_icon(self) -> None:
        """Set icon using Qt standard pixmaps."""
        style = self.style()
        icon_map = {
            MessageType.INFO: style.StandardPixmap.SP_MessageBoxInformation,
            MessageType.WARNING: style.StandardPixmap.SP_MessageBoxWarning,
            MessageType.ERROR: style.StandardPixmap.SP_MessageBoxCritical,
            MessageType.QUESTION: style.StandardPixmap.SP_MessageBoxQuestion,
        }

        pixmap_type = icon_map.get(
            self._message_type,
            style.StandardPixmap.SP_MessageBoxInformation,
        )
        pixmap = style.standardPixmap(pixmap_type)
        scaled = pixmap.scaled(
            24,
            24,
            Qt.AspectRatioMode.KeepAspectRatio,
            Qt.TransformationMode.SmoothTransformation,
        )
        self._icon_label.setPixmap(scaled)

    def _create_buttons(self, layout: QHBoxLayout) -> None:
        """Create buttons based on the buttons flag."""
        button_info = [
            (QMessageBox.StandardButton.Ok, "OK", True),
            (QMessageBox.StandardButton.Cancel, "Cancel", False),
            (QMessageBox.StandardButton.Yes, "Yes", True),
            (QMessageBox.StandardButton.No, "No", False),
            (QMessageBox.StandardButton.Retry, "Retry", True),
            (QMessageBox.StandardButton.Ignore, "Ignore", False),
            (QMessageBox.StandardButton.Close, "Close", False),
            (QMessageBox.StandardButton.Save, "Save", True),
            (QMessageBox.StandardButton.Discard, "Discard", False),
        ]

        first_primary = True
        for button_flag, text, is_primary in button_info:
            if self._buttons & button_flag:
                btn = QPushButton(text)

                # Set object name based on type
                btn_type = "primary" if is_primary else "secondary"
                btn.setObjectName(f"MessageDialog_Button_{btn_type}")

                # Make first primary button the default
                if is_primary and first_primary:
                    btn.setDefault(True)
                    first_primary = False

                btn.clicked.connect(
                    lambda checked=False, flag=button_flag: self._on_button_clicked(flag)
                )

                self._button_widgets[button_flag] = btn
                layout.addWidget(btn)

    @Slot()
    def _on_button_clicked(self, button_flag: QMessageBox.StandardButton) -> None:
        """
        Handle button click.

        Args:
            button_flag: The clicked button's flag.
        """
        self._result = button_flag
        self.button_clicked.emit(int(button_flag))
        self.accept()

    def get_result(self) -> QMessageBox.StandardButton:
        """
        Get the result after dialog closes.

        Returns:
            The button that was clicked.
        """
        return self._result

    @Slot(str)
    def _on_theme_changed(self, theme: str) -> None:
        """Update icon when theme changes."""
        self._setup_icon()

    # =========================================================================
    # STATIC CONVENIENCE METHODS
    # =========================================================================

    @staticmethod
    def info(
        parent: QWidget | None,
        title: str,
        message: str,
        buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
    ) -> QMessageBox.StandardButton:
        """
        Show an info message dialog.

        Args:
            parent: Parent widget.
            title: Dialog title.
            message: Message to display.
            buttons: Buttons to show.

        Returns:
            The button that was clicked.
        """
        dialog = MessageDialog(
            parent=parent,
            message_type=MessageType.INFO,
            title=title,
            message=message,
            buttons=buttons,
        )
        dialog.exec()
        return dialog.get_result()

    @staticmethod
    def warning(
        parent: QWidget | None,
        title: str,
        message: str,
        buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
    ) -> QMessageBox.StandardButton:
        """
        Show a warning message dialog.

        Args:
            parent: Parent widget.
            title: Dialog title.
            message: Warning message to display.
            buttons: Buttons to show.

        Returns:
            The button that was clicked.
        """
        dialog = MessageDialog(
            parent=parent,
            message_type=MessageType.WARNING,
            title=title,
            message=message,
            buttons=buttons,
        )
        dialog.exec()
        return dialog.get_result()

    @staticmethod
    def error(
        parent: QWidget | None,
        title: str,
        message: str,
        buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
    ) -> QMessageBox.StandardButton:
        """
        Show an error message dialog.

        Args:
            parent: Parent widget.
            title: Dialog title.
            message: Error message to display.
            buttons: Buttons to show.

        Returns:
            The button that was clicked.
        """
        dialog = MessageDialog(
            parent=parent,
            message_type=MessageType.ERROR,
            title=title,
            message=message,
            buttons=buttons,
        )
        dialog.exec()
        return dialog.get_result()

    @staticmethod
    def question(
        parent: QWidget | None,
        title: str,
        message: str,
        buttons: QMessageBox.StandardButton = (
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        ),
    ) -> QMessageBox.StandardButton:
        """
        Show a question dialog.

        Args:
            parent: Parent widget.
            title: Dialog title.
            message: Question to display.
            buttons: Buttons to show (default: Yes/No).

        Returns:
            The button that was clicked.
        """
        dialog = MessageDialog(
            parent=parent,
            message_type=MessageType.QUESTION,
            title=title,
            message=message,
            buttons=buttons,
        )
        dialog.exec()
        return dialog.get_result()
__init__(parent=None, message_type=MessageType.INFO, title='', message='', buttons=QMessageBox.StandardButton.Ok)

Initialize the message dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

None
message_type MessageType

Type of message (INFO, WARNING, ERROR, QUESTION).

INFO
title str

Dialog title.

''
message str

Message to display.

''
buttons StandardButton

Standard buttons to show.

Ok
Source code in src\shared_services\prompt_dialogs\message_box\message_dialog.py
def __init__(
    self,
    parent: QWidget | None = None,
    message_type: MessageType = MessageType.INFO,
    title: str = "",
    message: str = "",
    buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
) -> None:
    """
    Initialize the message dialog.

    Args:
        parent: Parent widget.
        message_type: Type of message (INFO, WARNING, ERROR, QUESTION).
        title: Dialog title.
        message: Message to display.
        buttons: Standard buttons to show.
    """
    super().__init__(
        parent=parent,
        stylesheets=[DialogStylesheets.Main, DialogStylesheets.MessageDialog],
    )

    self._message_type = message_type
    self._title = title or _DEFAULT_TITLES.get(message_type, "Message")
    self._message = message
    self._buttons = buttons
    self._result = QMessageBox.StandardButton.NoButton
    self._button_widgets: Dict[QMessageBox.StandardButton, QPushButton] = {}

    self._setup_ui()
    self._setup_icon()
error(parent, title, message, buttons=QMessageBox.StandardButton.Ok) staticmethod

Show an error message dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Error message to display.

required
buttons StandardButton

Buttons to show.

Ok

Returns:

Type Description
StandardButton

The button that was clicked.

Source code in src\shared_services\prompt_dialogs\message_box\message_dialog.py
@staticmethod
def error(
    parent: QWidget | None,
    title: str,
    message: str,
    buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
) -> QMessageBox.StandardButton:
    """
    Show an error message dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Error message to display.
        buttons: Buttons to show.

    Returns:
        The button that was clicked.
    """
    dialog = MessageDialog(
        parent=parent,
        message_type=MessageType.ERROR,
        title=title,
        message=message,
        buttons=buttons,
    )
    dialog.exec()
    return dialog.get_result()
get_result()

Get the result after dialog closes.

Returns:

Type Description
StandardButton

The button that was clicked.

Source code in src\shared_services\prompt_dialogs\message_box\message_dialog.py
def get_result(self) -> QMessageBox.StandardButton:
    """
    Get the result after dialog closes.

    Returns:
        The button that was clicked.
    """
    return self._result
info(parent, title, message, buttons=QMessageBox.StandardButton.Ok) staticmethod

Show an info message dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Message to display.

required
buttons StandardButton

Buttons to show.

Ok

Returns:

Type Description
StandardButton

The button that was clicked.

Source code in src\shared_services\prompt_dialogs\message_box\message_dialog.py
@staticmethod
def info(
    parent: QWidget | None,
    title: str,
    message: str,
    buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
) -> QMessageBox.StandardButton:
    """
    Show an info message dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Message to display.
        buttons: Buttons to show.

    Returns:
        The button that was clicked.
    """
    dialog = MessageDialog(
        parent=parent,
        message_type=MessageType.INFO,
        title=title,
        message=message,
        buttons=buttons,
    )
    dialog.exec()
    return dialog.get_result()
question(parent, title, message, buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) staticmethod

Show a question dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Question to display.

required
buttons StandardButton

Buttons to show (default: Yes/No).

Yes | No

Returns:

Type Description
StandardButton

The button that was clicked.

Source code in src\shared_services\prompt_dialogs\message_box\message_dialog.py
@staticmethod
def question(
    parent: QWidget | None,
    title: str,
    message: str,
    buttons: QMessageBox.StandardButton = (
        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    ),
) -> QMessageBox.StandardButton:
    """
    Show a question dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Question to display.
        buttons: Buttons to show (default: Yes/No).

    Returns:
        The button that was clicked.
    """
    dialog = MessageDialog(
        parent=parent,
        message_type=MessageType.QUESTION,
        title=title,
        message=message,
        buttons=buttons,
    )
    dialog.exec()
    return dialog.get_result()
warning(parent, title, message, buttons=QMessageBox.StandardButton.Ok) staticmethod

Show a warning message dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Warning message to display.

required
buttons StandardButton

Buttons to show.

Ok

Returns:

Type Description
StandardButton

The button that was clicked.

Source code in src\shared_services\prompt_dialogs\message_box\message_dialog.py
@staticmethod
def warning(
    parent: QWidget | None,
    title: str,
    message: str,
    buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
) -> QMessageBox.StandardButton:
    """
    Show a warning message dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Warning message to display.
        buttons: Buttons to show.

    Returns:
        The button that was clicked.
    """
    dialog = MessageDialog(
        parent=parent,
        message_type=MessageType.WARNING,
        title=title,
        message=message,
        buttons=buttons,
    )
    dialog.exec()
    return dialog.get_result()

MessageType

Bases: Enum

Types of message dialogs.

Source code in src\shared_services\prompt_dialogs\message_box\message_dialog.py
class MessageType(Enum):
    """Types of message dialogs."""

    INFO = auto()
    WARNING = auto()
    ERROR = auto()
    QUESTION = auto()

MultiButtonDialog

Bases: ThemeAwareDialog

Dialog presenting an arbitrary set of styled buttons.

Supports outlined (secondary), filled (primary), and hold-to-confirm button styles. When a hold-to-confirm button arms, the accent header bar transitions to red to reinforce the destructive action.

Source code in src\shared_services\prompt_dialogs\multi_button\multi_button_dialog.py
class MultiButtonDialog(ThemeAwareDialog):
    """Dialog presenting an arbitrary set of styled buttons.

    Supports outlined (secondary), filled (primary), and hold-to-confirm
    button styles. When a hold-to-confirm button arms, the accent header
    bar transitions to red to reinforce the destructive action.
    """

    # Preset mappings for DialogStyle
    _STYLE_ACCENT_COLORS = {
        DialogStyle.INFO: QColor(8, 145, 178),
        DialogStyle.WARNING: QColor(234, 88, 12),
        DialogStyle.ERROR: QColor(220, 38, 38),
    }

    def __init__(
        self,
        parent: Optional[QWidget] = None,
        title: str = "",
        message: str = "",
        buttons: Optional[List[ButtonDef]] = None,
        icon_path_def=None,
        accent_color: Optional[QColor] = None,
        dialog_style: Optional[DialogStyle] = None,
    ) -> None:
        super().__init__(
            parent=parent,
            stylesheets=[DialogStylesheets.Main, DialogStylesheets.MultiButtonDialog],
        )

        self._title_text = title
        self._message_text = message
        self._button_defs = buttons or []
        self._dialog_style = dialog_style

        # Style preset provides defaults; explicit icon/accent override the preset
        self._icon_path_def = icon_path_def or self._resolve_style_icon(dialog_style)
        self._base_accent_color = (
            accent_color
            or self._STYLE_ACCENT_COLORS.get(dialog_style)
            or QColor(37, 99, 235)
        )
        self._accent_color = QColor(self._base_accent_color)
        self._result: Optional[str] = None

        self._setup_ui()
        self._setup_icon()

    def _setup_ui(self) -> None:
        self.setObjectName("MultiButtonDialog")
        self.setWindowTitle(self._title_text)
        self.setMinimumWidth(440 + 2 * self._SHADOW_MARGIN)
        self.setMaximumWidth(560 + 2 * self._SHADOW_MARGIN)

        dialog_layout = QVBoxLayout(self)
        dialog_layout.setContentsMargins(
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
        )

        self._container = QFrame()
        self._container.setObjectName("MultiButtonDialog_Container")

        container_layout = QVBoxLayout(self._container)
        container_layout.setSpacing(10)
        container_layout.setContentsMargins(14, 12, 14, 12)

        # Content area (icon + text)
        header_layout = QHBoxLayout()
        header_layout.setSpacing(10)

        self._icon_label = QLabel()
        self._icon_label.setObjectName("MultiButtonDialog_Icon")
        self._icon_label.setFixedSize(28, 28)
        self._icon_label.setScaledContents(False)
        self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        header_layout.addWidget(self._icon_label, 0, Qt.AlignmentFlag.AlignTop)

        text_layout = QVBoxLayout()
        text_layout.setSpacing(4)

        self._title_label = QLabel(self._title_text)
        self._title_label.setObjectName("MultiButtonDialog_Title")
        text_layout.addWidget(self._title_label)

        self._message_label = QLabel(self._message_text)
        self._message_label.setObjectName("MultiButtonDialog_Message")
        self._message_label.setWordWrap(True)
        text_layout.addWidget(self._message_label)

        header_layout.addLayout(text_layout, 1)
        container_layout.addLayout(header_layout)

        # Button row
        button_layout = QHBoxLayout()
        button_layout.setSpacing(6)
        button_layout.addStretch()

        for btn_def in self._button_defs:
            if btn_def.style == "hold_to_confirm":
                btn = HoldToConfirmButton(btn_def.text)
                btn.setObjectName("MultiButtonDialog_Button_hold")
                btn.confirmed.connect(
                    lambda res=btn_def.result: self._on_result(res)
                )
                btn.armed_changed.connect(self._on_armed_changed)
            elif btn_def.style == "filled":
                btn = QPushButton(btn_def.text)
                btn.setObjectName("MultiButtonDialog_Button_primary")
                btn.clicked.connect(
                    lambda checked=False, res=btn_def.result: self._on_result(res)
                )
            else:
                btn = QPushButton(btn_def.text)
                btn.setObjectName("MultiButtonDialog_Button_secondary")
                btn.clicked.connect(
                    lambda checked=False, res=btn_def.result: self._on_result(res)
                )

            button_layout.addWidget(btn)

        container_layout.addLayout(button_layout)
        dialog_layout.addWidget(self._container)

    @staticmethod
    def _resolve_style_icon(style: Optional[DialogStyle]):
        """Return the icon PathDef for a DialogStyle preset, or None."""
        if style is None:
            return None
        try:
            from src.shared_services.rendering.icons.icon_paths import Icons
            return {
                DialogStyle.INFO: Icons.Action.Info,
                DialogStyle.WARNING: Icons.Alert.Warning,
                DialogStyle.ERROR: Icons.Alert.Error,
            }.get(style)
        except ImportError:
            return None

    def _setup_icon(self) -> None:
        if self._icon_path_def is None:
            # Default to question icon when no style preset is set
            try:
                from src.shared_services.rendering.icons.icon_paths import Icons
                self._icon_path_def = Icons.Action.Help
            except ImportError:
                return

        try:
            from src.shared_services.rendering.icons.api import render_svg
            self._icon_label.setPixmap(render_svg(self._icon_path_def, size=24))
        except (ImportError, FileNotFoundError):
            pass

    @Slot(bool)
    def _on_armed_changed(self, armed: bool) -> None:
        if armed:
            self._accent_color = QColor(220, 38, 38)
        else:
            self._accent_color = QColor(self._base_accent_color)
        self.update()

    def _on_result(self, result: str) -> None:
        self._result = result
        self.accept()

    def get_result(self) -> Optional[str]:
        return self._result

    @Slot(str)
    def _on_theme_changed(self, theme: str) -> None:
        self._setup_icon()

    # -- Static convenience method ----------------------------------------

    @staticmethod
    def show_dialog(
        parent: Optional[QWidget],
        title: str,
        message: str,
        buttons: List[ButtonDef],
        icon=None,
        accent_color: Optional[QColor] = None,
        dialog_style: Optional[DialogStyle] = None,
    ) -> Optional[str]:
        """Show a multi-button dialog and return the selected result string.

        Args:
            parent: Parent widget.
            title: Dialog title.
            message: Message to display.
            buttons: List of ButtonDef defining the buttons.
            icon: Optional icon PathDef. Overrides dialog_style icon.
            accent_color: Optional accent QColor. Overrides dialog_style color.
            dialog_style: Optional preset (INFO, WARNING, ERROR) that sets
                the icon and accent color. Explicit icon/accent_color take
                precedence over the preset.

        Returns:
            The result string of the clicked button, or None if closed.
        """
        dialog = MultiButtonDialog(
            parent=parent,
            title=title,
            message=message,
            buttons=buttons,
            icon_path_def=icon,
            accent_color=accent_color,
            dialog_style=dialog_style,
        )
        dialog.exec()
        return dialog.get_result()
show_dialog(parent, title, message, buttons, icon=None, accent_color=None, dialog_style=None) staticmethod

Show a multi-button dialog and return the selected result string.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget.

required
title str

Dialog title.

required
message str

Message to display.

required
buttons List[ButtonDef]

List of ButtonDef defining the buttons.

required
icon

Optional icon PathDef. Overrides dialog_style icon.

None
accent_color Optional[QColor]

Optional accent QColor. Overrides dialog_style color.

None
dialog_style Optional[DialogStyle]

Optional preset (INFO, WARNING, ERROR) that sets the icon and accent color. Explicit icon/accent_color take precedence over the preset.

None

Returns:

Type Description
Optional[str]

The result string of the clicked button, or None if closed.

Source code in src\shared_services\prompt_dialogs\multi_button\multi_button_dialog.py
@staticmethod
def show_dialog(
    parent: Optional[QWidget],
    title: str,
    message: str,
    buttons: List[ButtonDef],
    icon=None,
    accent_color: Optional[QColor] = None,
    dialog_style: Optional[DialogStyle] = None,
) -> Optional[str]:
    """Show a multi-button dialog and return the selected result string.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Message to display.
        buttons: List of ButtonDef defining the buttons.
        icon: Optional icon PathDef. Overrides dialog_style icon.
        accent_color: Optional accent QColor. Overrides dialog_style color.
        dialog_style: Optional preset (INFO, WARNING, ERROR) that sets
            the icon and accent color. Explicit icon/accent_color take
            precedence over the preset.

    Returns:
        The result string of the clicked button, or None if closed.
    """
    dialog = MultiButtonDialog(
        parent=parent,
        title=title,
        message=message,
        buttons=buttons,
        icon_path_def=icon,
        accent_color=accent_color,
        dialog_style=dialog_style,
    )
    dialog.exec()
    return dialog.get_result()

SignDialog

Bases: ThemeAwareDialog

Dialog for entering a commit/sign message before releasing.

When allow_cancel is False the dialog cannot be dismissed via the close button, Escape key, or a cancel button -- the user must confirm.

Source code in src\shared_services\prompt_dialogs\sign\sign_dialog.py
class SignDialog(ThemeAwareDialog):
    """Dialog for entering a commit/sign message before releasing.

    When ``allow_cancel`` is False the dialog cannot be dismissed via
    the close button, Escape key, or a cancel button -- the user must
    confirm.
    """

    def __init__(
        self,
        parent: Optional[QWidget] = None,
        title: str = "Signieren",
        instruction: str = "Bitte geben Sie eine Beschreibung ein.",
        placeholder: str = "Unkommentiert",
        sign_text: str = "Signieren und freigeben",
        allow_cancel: bool = False,
        default_message: str = "Unkommentiert",
        accent_color: Optional[QColor] = None,
    ) -> None:
        super().__init__(
            parent=parent,
            stylesheets=[DialogStylesheets.Main, DialogStylesheets.SignDialog],
        )

        self._title_text = title
        self._instruction_text = instruction
        self._placeholder = placeholder
        self._sign_text = sign_text
        self._allow_cancel = allow_cancel
        self._default_message = default_message
        self._accent_color = accent_color or QColor(22, 163, 74)

        self._confirmed = False
        self._commit_message = ""

        self._setup_ui()
        self._setup_icon()
        self._connect_signals()

    def _setup_ui(self) -> None:
        self.setObjectName("SignDialog")
        self.setWindowTitle(self._title_text)
        self.setMinimumWidth(480 + 2 * self._SHADOW_MARGIN)
        self.setMaximumWidth(560 + 2 * self._SHADOW_MARGIN)

        dialog_layout = QVBoxLayout(self)
        dialog_layout.setContentsMargins(
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
        )

        self._container = QFrame()
        self._container.setObjectName("SignDialog_Container")

        container_layout = QVBoxLayout(self._container)
        container_layout.setSpacing(10)
        container_layout.setContentsMargins(14, 12, 14, 12)

        # Header with icon
        header_layout = QHBoxLayout()
        header_layout.setSpacing(10)

        self._icon_label = QLabel()
        self._icon_label.setObjectName("SignDialog_Icon")
        self._icon_label.setFixedSize(28, 28)
        self._icon_label.setScaledContents(False)
        self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        header_layout.addWidget(self._icon_label, 0, Qt.AlignmentFlag.AlignTop)

        # Title
        self._title_label = QLabel(self._title_text)
        self._title_label.setObjectName("SignDialog_Title")
        header_layout.addWidget(self._title_label, 1)

        container_layout.addLayout(header_layout)

        # Instruction
        self._instruction_label = QLabel(self._instruction_text)
        self._instruction_label.setObjectName("SignDialog_Instruction")
        self._instruction_label.setWordWrap(True)
        container_layout.addWidget(self._instruction_label)

        # Text edit
        self._text_edit = QTextEdit()
        self._text_edit.setObjectName("SignDialog_TextEdit")
        self._text_edit.setMinimumHeight(100)
        self._text_edit.setPlaceholderText(self._placeholder)
        container_layout.addWidget(self._text_edit)

        # Button row
        button_layout = QHBoxLayout()
        button_layout.setSpacing(6)
        button_layout.addStretch()

        if self._allow_cancel:
            self._cancel_button = QPushButton("Abbrechen")
            self._cancel_button.setObjectName("SignDialog_Button_secondary")
            button_layout.addWidget(self._cancel_button)
        else:
            self._cancel_button = None

        self._sign_button = QPushButton(self._sign_text)
        self._sign_button.setObjectName("SignDialog_Button_primary")
        self._sign_button.setDefault(True)
        self._sign_button.setEnabled(False)
        button_layout.addWidget(self._sign_button)

        container_layout.addLayout(button_layout)
        dialog_layout.addWidget(self._container)

    def _setup_icon(self) -> None:
        try:
            from src.shared_services.rendering.icons.icon_paths import Icons
            from src.shared_services.rendering.icons.api import render_svg
            self._icon_label.setPixmap(render_svg(Icons.Action.Check, size=24))
        except (ImportError, FileNotFoundError, AttributeError):
            try:
                from src.shared_services.rendering.icons.icon_paths import Icons
                from src.shared_services.rendering.icons.api import render_svg
                self._icon_label.setPixmap(render_svg(Icons.Action.Info, size=24))
            except (ImportError, FileNotFoundError):
                pass

    def _connect_signals(self) -> None:
        self._text_edit.textChanged.connect(self._on_text_changed)
        self._sign_button.clicked.connect(self._on_sign_clicked)
        if self._cancel_button is not None:
            self._cancel_button.clicked.connect(self.reject)

    @Slot()
    def _on_text_changed(self) -> None:
        has_text = bool(self._text_edit.toPlainText().strip())
        self._sign_button.setEnabled(has_text)

    @Slot()
    def _on_sign_clicked(self) -> None:
        text = self._text_edit.toPlainText().strip()
        self._commit_message = text if text else self._default_message
        self._confirmed = True
        self.accept()

    def keyPressEvent(self, event) -> None:
        if not self._allow_cancel and event.key() == Qt.Key.Key_Escape:
            return
        super().keyPressEvent(event)

    def closeEvent(self, event) -> None:
        if not self._allow_cancel:
            event.ignore()
            return
        super().closeEvent(event)

    @Slot(str)
    def _on_theme_changed(self, theme: str) -> None:
        self._setup_icon()

    # -- Static convenience method ----------------------------------------

    @staticmethod
    def get_commit_message(
        parent: Optional[QWidget] = None,
        title: str = "Signieren",
        instruction: str = "Bitte geben Sie eine Beschreibung ein.",
        placeholder: str = "Unkommentiert",
        sign_text: str = "Signieren und freigeben",
        allow_cancel: bool = False,
        default_message: str = "Unkommentiert",
        accent_color: Optional[QColor] = None,
    ) -> Tuple[bool, str]:
        """Show a sign dialog and return the result.

        Args:
            parent: Parent widget.
            title: Dialog title.
            instruction: Instruction text shown above the text edit.
            placeholder: Placeholder text in the text edit.
            sign_text: Label for the sign/confirm button.
            allow_cancel: If False, dialog cannot be dismissed without signing.
            default_message: Message used when text is empty on confirm.
            accent_color: Optional accent QColor for the header bar.

        Returns:
            Tuple of (confirmed, message). confirmed is True when the user
            clicked the sign button. message is the entered text or
            default_message if empty.
        """
        dialog = SignDialog(
            parent=parent,
            title=title,
            instruction=instruction,
            placeholder=placeholder,
            sign_text=sign_text,
            allow_cancel=allow_cancel,
            default_message=default_message,
            accent_color=accent_color,
        )
        dialog.exec()
        return dialog._confirmed, dialog._commit_message
get_commit_message(parent=None, title='Signieren', instruction='Bitte geben Sie eine Beschreibung ein.', placeholder='Unkommentiert', sign_text='Signieren und freigeben', allow_cancel=False, default_message='Unkommentiert', accent_color=None) staticmethod

Show a sign dialog and return the result.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget.

None
title str

Dialog title.

'Signieren'
instruction str

Instruction text shown above the text edit.

'Bitte geben Sie eine Beschreibung ein.'
placeholder str

Placeholder text in the text edit.

'Unkommentiert'
sign_text str

Label for the sign/confirm button.

'Signieren und freigeben'
allow_cancel bool

If False, dialog cannot be dismissed without signing.

False
default_message str

Message used when text is empty on confirm.

'Unkommentiert'
accent_color Optional[QColor]

Optional accent QColor for the header bar.

None

Returns:

Type Description
bool

Tuple of (confirmed, message). confirmed is True when the user

str

clicked the sign button. message is the entered text or

Tuple[bool, str]

default_message if empty.

Source code in src\shared_services\prompt_dialogs\sign\sign_dialog.py
@staticmethod
def get_commit_message(
    parent: Optional[QWidget] = None,
    title: str = "Signieren",
    instruction: str = "Bitte geben Sie eine Beschreibung ein.",
    placeholder: str = "Unkommentiert",
    sign_text: str = "Signieren und freigeben",
    allow_cancel: bool = False,
    default_message: str = "Unkommentiert",
    accent_color: Optional[QColor] = None,
) -> Tuple[bool, str]:
    """Show a sign dialog and return the result.

    Args:
        parent: Parent widget.
        title: Dialog title.
        instruction: Instruction text shown above the text edit.
        placeholder: Placeholder text in the text edit.
        sign_text: Label for the sign/confirm button.
        allow_cancel: If False, dialog cannot be dismissed without signing.
        default_message: Message used when text is empty on confirm.
        accent_color: Optional accent QColor for the header bar.

    Returns:
        Tuple of (confirmed, message). confirmed is True when the user
        clicked the sign button. message is the entered text or
        default_message if empty.
    """
    dialog = SignDialog(
        parent=parent,
        title=title,
        instruction=instruction,
        placeholder=placeholder,
        sign_text=sign_text,
        allow_cancel=allow_cancel,
        default_message=default_message,
        accent_color=accent_color,
    )
    dialog.exec()
    return dialog._confirmed, dialog._commit_message

SoundPatch

Monkey patch manager for silent message boxes.

Replaces QMessageBox.setIcon() with a custom implementation that uses setIconPixmap() instead, which does not trigger Windows system sounds. The custom implementation uses SVG icons that match the application theme.

Example

Applying the patch at startup::

from src.shared_services.dialogs.sound.sound_patch import SoundPatch

def main():
    app = QApplication(sys.argv)
    SoundPatch.apply()
    # ... rest of application
Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
class SoundPatch:
    """
    Monkey patch manager for silent message boxes.

    Replaces QMessageBox.setIcon() with a custom implementation that uses
    setIconPixmap() instead, which does not trigger Windows system sounds.
    The custom implementation uses SVG icons that match the application theme.

    Example:
        Applying the patch at startup::

            from src.shared_services.dialogs.sound.sound_patch import SoundPatch

            def main():
                app = QApplication(sys.argv)
                SoundPatch.apply()
                # ... rest of application
    """

    _patched: ClassVar[bool] = False
    _original_setIcon: ClassVar[Optional[callable]] = None
    _icon_cache: ClassVar[Dict[tuple, QPixmap]] = {}

    # Icon mapping to PathDef definitions
    _ICON_MAPPING: ClassVar[Dict[QMessageBox.Icon, object]] = {
        QMessageBox.Icon.Information: Icons.Action.Info,
        QMessageBox.Icon.Warning: Icons.Alert.Warning,
        QMessageBox.Icon.Critical: Icons.Alert.Warning,
        QMessageBox.Icon.Question: Icons.Action.Help,
    }

    @classmethod
    def apply(cls) -> None:
        """
        Apply the monkey patch to disable QMessageBox sounds globally.

        This patches QMessageBox.setIcon() to use setIconPixmap() instead,
        which displays the icon without triggering Windows system sounds.

        Safe to call multiple times - only applies once.
        """
        if cls._patched:
            return

        # Store original method
        cls._original_setIcon = QMessageBox.setIcon

        def silent_setIcon(self: QMessageBox, icon: QMessageBox.Icon) -> None:
            """Replacement setIcon that uses setIconPixmap with custom icons."""
            try:
                pixmap = cls._get_icon_pixmap(icon)
                if not pixmap.isNull():
                    self.setIconPixmap(pixmap)
                    return

            except (FileNotFoundError, RuntimeError):
                pass

            # Fallback: use NoIcon to prevent system sound
            if cls._original_setIcon is not None:
                cls._original_setIcon(self, QMessageBox.Icon.NoIcon)

        # Apply the patch
        QMessageBox.setIcon = silent_setIcon
        cls._patched = True

        cls._log_info("Silent MessageBox patch applied")

    @classmethod
    def remove(cls) -> None:
        """
        Remove the monkey patch.

        Restores the original QMessageBox.setIcon() behavior.
        Primarily useful for testing purposes.
        """
        if cls._patched and cls._original_setIcon is not None:
            QMessageBox.setIcon = cls._original_setIcon
            cls._patched = False
            cls._log_info("Silent MessageBox patch removed")

    @classmethod
    def is_patched(cls) -> bool:
        """
        Check if the patch is currently applied.

        Returns:
            True if the patch is active.
        """
        return cls._patched

    @classmethod
    def clear_cache(cls) -> None:
        """
        Clear the icon pixmap cache.

        Call this when the theme changes to ensure icons are re-rendered
        with the correct colors.
        """
        cls._icon_cache.clear()

    @classmethod
    def refresh_for_theme(cls) -> None:
        """
        Refresh icons for a theme change.

        Clears the cache so icons will be re-rendered with theme-appropriate
        colors on next use.
        """
        cls.clear_cache()
        cls._log_info("MessageBox icon cache cleared for theme change")

    @classmethod
    def _get_icon_pixmap(
        cls,
        message_icon: QMessageBox.Icon,
        size: int = 48,
    ) -> QPixmap:
        """
        Get pixmap for the specified message icon type.

        Args:
            message_icon: The QMessageBox icon type.
            size: Icon size in pixels.

        Returns:
            QPixmap for the icon, or null pixmap if not available.
        """
        # Determine current theme
        is_dark = cls._is_dark_theme()

        # Check cache
        cache_key = (message_icon, size, is_dark)
        if cache_key in cls._icon_cache:
            return cls._icon_cache[cache_key]

        # Get PathDef for this icon type
        icon_def = cls._ICON_MAPPING.get(message_icon)
        if icon_def is None:
            return QPixmap()

        # Resolve path
        try:
            icon_path = get_path_str(icon_def)
        except (ValueError, FileNotFoundError):
            return QPixmap()

        # Render SVG to pixmap
        pixmap = cls._svg_to_pixmap(icon_path, size, is_dark)

        # Cache result
        cls._icon_cache[cache_key] = pixmap

        return pixmap

    @classmethod
    def _svg_to_pixmap(
        cls,
        svg_path: str,
        size: int,
        is_dark_theme: bool = False,
    ) -> QPixmap:
        """
        Convert SVG file to QPixmap with high DPI support.

        Args:
            svg_path: Path to the SVG file.
            size: Target size in pixels.
            is_dark_theme: Whether to colorize for dark theme.

        Returns:
            Rendered QPixmap.
        """
        # Get device pixel ratio for high DPI screens
        pixel_ratio = 1.0
        screen = QApplication.primaryScreen()
        if screen is not None:
            pixel_ratio = screen.devicePixelRatio()

        # Calculate actual size for high DPI
        actual_size = int(size * pixel_ratio)
        qsize = QSize(actual_size, actual_size)

        # Load and validate SVG
        renderer = QSvgRenderer(svg_path)
        if not renderer.isValid():
            return QPixmap()

        # Create pixmap with transparent background
        pixmap = QPixmap(qsize)
        pixmap.fill(Qt.GlobalColor.transparent)

        # Render the SVG
        painter = QPainter(pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)

        renderer.render(painter)

        # Colorize for non-light themes using centralized icon tint
        if is_dark_theme:
            from src.shared_services.rendering.constants.widget_colors import get_widget_colors
            from src.shared_services.rendering.stylesheets.api import get_theme
            tint = get_widget_colors(get_theme()).dialog.icon_tint
            painter.setCompositionMode(
                QPainter.CompositionMode.CompositionMode_SourceIn
            )
            painter.fillRect(pixmap.rect(), tint)

        painter.end()

        # Set device pixel ratio for proper scaling
        pixmap.setDevicePixelRatio(pixel_ratio)

        return pixmap

    @classmethod
    def _is_dark_theme(cls) -> bool:
        """
        Check if the current theme is dark.

        Returns:
            True if dark theme is active.
        """
        from src.shared_services.rendering.stylesheets.api import get_theme
        return get_theme() in ("dark", "gray")

    @classmethod
    def _log_info(cls, message: str) -> None:
        """
        Log an info message.

        Args:
            message: Message to log.
        """
        try:
            from src.shared_services.logging.logger_factory import get_logger

            logger = get_logger()
            logger.info(message)
        except ImportError:
            pass
apply() classmethod

Apply the monkey patch to disable QMessageBox sounds globally.

This patches QMessageBox.setIcon() to use setIconPixmap() instead, which displays the icon without triggering Windows system sounds.

Safe to call multiple times - only applies once.

Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
@classmethod
def apply(cls) -> None:
    """
    Apply the monkey patch to disable QMessageBox sounds globally.

    This patches QMessageBox.setIcon() to use setIconPixmap() instead,
    which displays the icon without triggering Windows system sounds.

    Safe to call multiple times - only applies once.
    """
    if cls._patched:
        return

    # Store original method
    cls._original_setIcon = QMessageBox.setIcon

    def silent_setIcon(self: QMessageBox, icon: QMessageBox.Icon) -> None:
        """Replacement setIcon that uses setIconPixmap with custom icons."""
        try:
            pixmap = cls._get_icon_pixmap(icon)
            if not pixmap.isNull():
                self.setIconPixmap(pixmap)
                return

        except (FileNotFoundError, RuntimeError):
            pass

        # Fallback: use NoIcon to prevent system sound
        if cls._original_setIcon is not None:
            cls._original_setIcon(self, QMessageBox.Icon.NoIcon)

    # Apply the patch
    QMessageBox.setIcon = silent_setIcon
    cls._patched = True

    cls._log_info("Silent MessageBox patch applied")
clear_cache() classmethod

Clear the icon pixmap cache.

Call this when the theme changes to ensure icons are re-rendered with the correct colors.

Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
@classmethod
def clear_cache(cls) -> None:
    """
    Clear the icon pixmap cache.

    Call this when the theme changes to ensure icons are re-rendered
    with the correct colors.
    """
    cls._icon_cache.clear()
is_patched() classmethod

Check if the patch is currently applied.

Returns:

Type Description
bool

True if the patch is active.

Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
@classmethod
def is_patched(cls) -> bool:
    """
    Check if the patch is currently applied.

    Returns:
        True if the patch is active.
    """
    return cls._patched
refresh_for_theme() classmethod

Refresh icons for a theme change.

Clears the cache so icons will be re-rendered with theme-appropriate colors on next use.

Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
@classmethod
def refresh_for_theme(cls) -> None:
    """
    Refresh icons for a theme change.

    Clears the cache so icons will be re-rendered with theme-appropriate
    colors on next use.
    """
    cls.clear_cache()
    cls._log_info("MessageBox icon cache cleared for theme change")
remove() classmethod

Remove the monkey patch.

Restores the original QMessageBox.setIcon() behavior. Primarily useful for testing purposes.

Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
@classmethod
def remove(cls) -> None:
    """
    Remove the monkey patch.

    Restores the original QMessageBox.setIcon() behavior.
    Primarily useful for testing purposes.
    """
    if cls._patched and cls._original_setIcon is not None:
        QMessageBox.setIcon = cls._original_setIcon
        cls._patched = False
        cls._log_info("Silent MessageBox patch removed")

ThemeAwareDialog

Bases: QDialog

Base class for theme-aware dialogs.

Automatically registers with StylesheetManager for theme updates. Uses weak references to avoid memory leaks. Handles cleanup on close.

Subclasses should: - Call super().init() with appropriate stylesheets - Set a meaningful object name via setObjectName() - Override _on_theme_changed() if custom theme handling is needed

Attributes:

Name Type Description
dialog_closed Signal

Signal emitted when the dialog closes.

Example

Creating a themed dialog::

class InfoDialog(ThemeAwareDialog):
    def __init__(self, parent=None):
        super().__init__(
            parent=parent,
            stylesheets=[DialogStylesheets.Main],
        )
        self.setObjectName("InfoDialog")
Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
class ThemeAwareDialog(QDialog):
    """
    Base class for theme-aware dialogs.

    Automatically registers with StylesheetManager for theme updates.
    Uses weak references to avoid memory leaks. Handles cleanup on close.

    Subclasses should:
    - Call super().__init__() with appropriate stylesheets
    - Set a meaningful object name via setObjectName()
    - Override _on_theme_changed() if custom theme handling is needed

    Attributes:
        dialog_closed: Signal emitted when the dialog closes.

    Example:
        Creating a themed dialog::

            class InfoDialog(ThemeAwareDialog):
                def __init__(self, parent=None):
                    super().__init__(
                        parent=parent,
                        stylesheets=[DialogStylesheets.Main],
                    )
                    self.setObjectName("InfoDialog")
    """

    dialog_closed: Signal = Signal()

    _SHADOW_MARGIN = 14
    _BORDER_RADIUS = 10
    _HEADER_BAR_HEIGHT = 4

    def __init__(
        self,
        parent: QWidget | None = None,
        stylesheets: List[PathDef] | None = None,
    ) -> None:
        """
        Initialize the theme-aware dialog.

        Args:
            parent: Parent widget.
            stylesheets: List of PathDef stylesheet definitions to apply.
        """
        super().__init__(parent)

        self._stylesheets = stylesheets or []
        self._theme_callback_registered = False
        self._accent_color: Optional[QColor] = None

        # Configure common dialog properties - frameless for modern look
        self.setWindowFlags(
            Qt.WindowType.Dialog
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.NoDropShadowWindowHint
        )
        self.setModal(True)
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)

        # Register with stylesheet manager if stylesheets provided
        if self._stylesheets:
            self._register_stylesheets()

    def _register_stylesheets(self) -> None:
        """Register with StylesheetManager for automatic theme updates."""
        try:
            from src.shared_services.rendering.stylesheets.stylesheet_manager import (
                StylesheetManager,
            )

            manager = StylesheetManager.instance()
            manager.register(self, self._stylesheets)

            # Register theme change callback (only once)
            if not self._theme_callback_registered:
                manager.on_theme_change(self._on_theme_changed)
                self._theme_callback_registered = True

        except ImportError:
            # StylesheetManager not available - continue without styling
            pass

    @Slot(str)
    def _on_theme_changed(self, theme: str) -> None:
        """
        Handle theme change notification.

        Override this method in subclasses to perform custom actions
        when the theme changes (e.g., updating icons, refreshing content).

        Args:
            theme: New theme name ("light" or "dark").
        """
        # Base implementation does nothing - subclasses can override
        pass

    def get_current_theme(self) -> str:
        """
        Get the current theme.

        Returns:
            Current theme name ("light" or "dark").
        """
        try:
            from src.shared_services.rendering.stylesheets.stylesheet_manager import (
                StylesheetManager,
            )
            return StylesheetManager.instance().get_theme()
        except ImportError:
            return "light"

    def is_dark_theme(self) -> bool:
        """
        Check if the current theme is dark.

        Returns:
            True if dark theme is active.
        """
        return self.get_current_theme() == "dark"

    def get_theme_color(self, color_key: str) -> str:
        """
        Get a color value for the current theme.

        Args:
            color_key: Color key from ColorSystem (e.g., 'primary', 'error').

        Returns:
            Color value as hex string.
        """
        try:
            from src.shared_services.rendering.constants.colors import ColorSystem

            theme = self.get_current_theme()
            colors = ColorSystem.get_colors(theme)
            return colors.get(color_key, "#000000")
        except ImportError:
            return "#000000"

    def refresh_style(self, widget: QWidget | None = None) -> None:
        """
        Refresh the style of a widget after property changes.

        Call this after setting dynamic properties to trigger style update.

        Args:
            widget: Widget to refresh. If None, refreshes self.
        """
        target = widget or self
        target.style().unpolish(target)
        target.style().polish(target)
        target.update()

    def paintEvent(self, _event) -> None:
        """Draw soft shadow, rounded background, and optional header bar."""
        from src.shared_services.rendering.constants.widget_colors import get_widget_colors

        m = self._SHADOW_MARGIN
        r = self._BORDER_RADIUS

        p = QPainter(self)
        p.setRenderHint(QPainter.RenderHint.Antialiasing)

        # Shadow and border colors from centralized theme
        dc = get_widget_colors(self.get_current_theme()).dialog
        shadow_base = dc.shadow
        border_color = dc.border

        # Soft drop shadow (concentric rounded rects, quadratic falloff)
        base_alpha = shadow_base.alpha()
        for i in range(m):
            frac = (m - i) / m
            a = int(base_alpha * frac * frac)
            p.setPen(Qt.PenStyle.NoPen)
            p.setBrush(QColor(shadow_base.red(), shadow_base.green(),
                              shadow_base.blue(), a))
            inset = m - i
            rect = QRectF(inset, inset + 2,
                          self.width() - 2 * inset,
                          self.height() - 2 * inset - 2)
            p.drawRoundedRect(rect, r + i * 0.5, r + i * 0.5)

        # Background
        app = QApplication.instance()
        bg_color = app.palette().color(app.palette().ColorRole.Window) if app else QColor(255, 255, 255)
        bg_rect = QRectF(m, m, self.width() - 2 * m, self.height() - 2 * m)
        p.setBrush(bg_color)
        p.setPen(QPen(border_color, 1.0))
        p.drawRoundedRect(bg_rect, r, r)

        # Header bar
        if self._accent_color is not None:
            clip_path = QPainterPath()
            clip_path.addRoundedRect(bg_rect, r, r)
            p.setClipPath(clip_path)
            p.setPen(Qt.PenStyle.NoPen)
            p.setBrush(self._accent_color)
            p.drawRect(QRectF(m, m, bg_rect.width(), self._HEADER_BAR_HEIGHT))
            p.setClipping(False)

        p.end()

    def closeEvent(self, event: QCloseEvent) -> None:
        """
        Handle dialog close event.

        Unregisters from StylesheetManager and emits dialog_closed signal.

        Args:
            event: Close event.
        """
        try:
            # Unregister from stylesheet manager
            from src.shared_services.rendering.stylesheets.stylesheet_manager import (
                StylesheetManager,
            )
            StylesheetManager.instance().unregister(self)
        except (ImportError, RuntimeError):
            pass

        # Emit closed signal
        self.dialog_closed.emit()

        # Call parent close event
        super().closeEvent(event)

    def show_centered(self) -> None:
        """
        Show the dialog centered on the screen or parent.

        Positions the dialog in the center of the parent widget if available,
        otherwise centers on the primary screen.
        """
        self.show()

        if self.parent():
            # Center on parent
            parent_geometry = self.parent().geometry()
            x = parent_geometry.x() + (parent_geometry.width() - self.width()) // 2
            y = parent_geometry.y() + (parent_geometry.height() - self.height()) // 2
            self.move(x, y)
        else:
            # Center on screen
            try:
                from PySide6.QtWidgets import QApplication
                screen = QApplication.primaryScreen()
                if screen:
                    screen_geometry = screen.geometry()
                    x = (screen_geometry.width() - self.width()) // 2
                    y = (screen_geometry.height() - self.height()) // 2
                    self.move(x, y)
            except Exception:
                pass
__init__(parent=None, stylesheets=None)

Initialize the theme-aware dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

None
stylesheets List[PathDef] | None

List of PathDef stylesheet definitions to apply.

None
Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
def __init__(
    self,
    parent: QWidget | None = None,
    stylesheets: List[PathDef] | None = None,
) -> None:
    """
    Initialize the theme-aware dialog.

    Args:
        parent: Parent widget.
        stylesheets: List of PathDef stylesheet definitions to apply.
    """
    super().__init__(parent)

    self._stylesheets = stylesheets or []
    self._theme_callback_registered = False
    self._accent_color: Optional[QColor] = None

    # Configure common dialog properties - frameless for modern look
    self.setWindowFlags(
        Qt.WindowType.Dialog
        | Qt.WindowType.FramelessWindowHint
        | Qt.WindowType.NoDropShadowWindowHint
    )
    self.setModal(True)
    self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
    self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)

    # Register with stylesheet manager if stylesheets provided
    if self._stylesheets:
        self._register_stylesheets()
closeEvent(event)

Handle dialog close event.

Unregisters from StylesheetManager and emits dialog_closed signal.

Parameters:

Name Type Description Default
event QCloseEvent

Close event.

required
Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
def closeEvent(self, event: QCloseEvent) -> None:
    """
    Handle dialog close event.

    Unregisters from StylesheetManager and emits dialog_closed signal.

    Args:
        event: Close event.
    """
    try:
        # Unregister from stylesheet manager
        from src.shared_services.rendering.stylesheets.stylesheet_manager import (
            StylesheetManager,
        )
        StylesheetManager.instance().unregister(self)
    except (ImportError, RuntimeError):
        pass

    # Emit closed signal
    self.dialog_closed.emit()

    # Call parent close event
    super().closeEvent(event)
get_current_theme()

Get the current theme.

Returns:

Type Description
str

Current theme name ("light" or "dark").

Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
def get_current_theme(self) -> str:
    """
    Get the current theme.

    Returns:
        Current theme name ("light" or "dark").
    """
    try:
        from src.shared_services.rendering.stylesheets.stylesheet_manager import (
            StylesheetManager,
        )
        return StylesheetManager.instance().get_theme()
    except ImportError:
        return "light"
get_theme_color(color_key)

Get a color value for the current theme.

Parameters:

Name Type Description Default
color_key str

Color key from ColorSystem (e.g., 'primary', 'error').

required

Returns:

Type Description
str

Color value as hex string.

Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
def get_theme_color(self, color_key: str) -> str:
    """
    Get a color value for the current theme.

    Args:
        color_key: Color key from ColorSystem (e.g., 'primary', 'error').

    Returns:
        Color value as hex string.
    """
    try:
        from src.shared_services.rendering.constants.colors import ColorSystem

        theme = self.get_current_theme()
        colors = ColorSystem.get_colors(theme)
        return colors.get(color_key, "#000000")
    except ImportError:
        return "#000000"
is_dark_theme()

Check if the current theme is dark.

Returns:

Type Description
bool

True if dark theme is active.

Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
def is_dark_theme(self) -> bool:
    """
    Check if the current theme is dark.

    Returns:
        True if dark theme is active.
    """
    return self.get_current_theme() == "dark"
paintEvent(_event)

Draw soft shadow, rounded background, and optional header bar.

Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
def paintEvent(self, _event) -> None:
    """Draw soft shadow, rounded background, and optional header bar."""
    from src.shared_services.rendering.constants.widget_colors import get_widget_colors

    m = self._SHADOW_MARGIN
    r = self._BORDER_RADIUS

    p = QPainter(self)
    p.setRenderHint(QPainter.RenderHint.Antialiasing)

    # Shadow and border colors from centralized theme
    dc = get_widget_colors(self.get_current_theme()).dialog
    shadow_base = dc.shadow
    border_color = dc.border

    # Soft drop shadow (concentric rounded rects, quadratic falloff)
    base_alpha = shadow_base.alpha()
    for i in range(m):
        frac = (m - i) / m
        a = int(base_alpha * frac * frac)
        p.setPen(Qt.PenStyle.NoPen)
        p.setBrush(QColor(shadow_base.red(), shadow_base.green(),
                          shadow_base.blue(), a))
        inset = m - i
        rect = QRectF(inset, inset + 2,
                      self.width() - 2 * inset,
                      self.height() - 2 * inset - 2)
        p.drawRoundedRect(rect, r + i * 0.5, r + i * 0.5)

    # Background
    app = QApplication.instance()
    bg_color = app.palette().color(app.palette().ColorRole.Window) if app else QColor(255, 255, 255)
    bg_rect = QRectF(m, m, self.width() - 2 * m, self.height() - 2 * m)
    p.setBrush(bg_color)
    p.setPen(QPen(border_color, 1.0))
    p.drawRoundedRect(bg_rect, r, r)

    # Header bar
    if self._accent_color is not None:
        clip_path = QPainterPath()
        clip_path.addRoundedRect(bg_rect, r, r)
        p.setClipPath(clip_path)
        p.setPen(Qt.PenStyle.NoPen)
        p.setBrush(self._accent_color)
        p.drawRect(QRectF(m, m, bg_rect.width(), self._HEADER_BAR_HEIGHT))
        p.setClipping(False)

    p.end()
refresh_style(widget=None)

Refresh the style of a widget after property changes.

Call this after setting dynamic properties to trigger style update.

Parameters:

Name Type Description Default
widget QWidget | None

Widget to refresh. If None, refreshes self.

None
Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
def refresh_style(self, widget: QWidget | None = None) -> None:
    """
    Refresh the style of a widget after property changes.

    Call this after setting dynamic properties to trigger style update.

    Args:
        widget: Widget to refresh. If None, refreshes self.
    """
    target = widget or self
    target.style().unpolish(target)
    target.style().polish(target)
    target.update()
show_centered()

Show the dialog centered on the screen or parent.

Positions the dialog in the center of the parent widget if available, otherwise centers on the primary screen.

Source code in src\shared_services\prompt_dialogs\base\theme_aware_dialog.py
def show_centered(self) -> None:
    """
    Show the dialog centered on the screen or parent.

    Positions the dialog in the center of the parent widget if available,
    otherwise centers on the primary screen.
    """
    self.show()

    if self.parent():
        # Center on parent
        parent_geometry = self.parent().geometry()
        x = parent_geometry.x() + (parent_geometry.width() - self.width()) // 2
        y = parent_geometry.y() + (parent_geometry.height() - self.height()) // 2
        self.move(x, y)
    else:
        # Center on screen
        try:
            from PySide6.QtWidgets import QApplication
            screen = QApplication.primaryScreen()
            if screen:
                screen_geometry = screen.geometry()
                x = (screen_geometry.width() - self.width()) // 2
                y = (screen_geometry.height() - self.height()) // 2
                self.move(x, y)
        except Exception:
            pass

UnsavedChangesDialog

Bases: ThemeAwareDialog

Theme-aware dialog for unsaved changes with Save/Discard/Cancel.

Uses the same visual style as MessageDialog (warning accent). Supports 2-button mode (save/discard) or 3-button mode (save/discard/cancel) via the show_cancel parameter.

Source code in src\shared_services\prompt_dialogs\unsaved_changes\unsaved_changes_dialog.py
class UnsavedChangesDialog(ThemeAwareDialog):
    """Theme-aware dialog for unsaved changes with Save/Discard/Cancel.

    Uses the same visual style as MessageDialog (warning accent).
    Supports 2-button mode (save/discard) or 3-button mode
    (save/discard/cancel) via the ``show_cancel`` parameter.
    """

    def __init__(
        self,
        parent: Optional[QWidget] = None,
        title: str = "Ungespeicherte Änderungen",
        message: str = "",
        save_text: str = "Speichern",
        discard_text: str = "Verwerfen",
        cancel_text: str = "Abbrechen",
        show_cancel: bool = True,
    ) -> None:
        super().__init__(
            parent=parent,
            stylesheets=[DialogStylesheets.Main, DialogStylesheets.MessageDialog],
        )

        self._title_text = title
        self._message_text = message
        self._save_text = save_text
        self._discard_text = discard_text
        self._cancel_text = cancel_text
        self._show_cancel = show_cancel
        self._result = UnsavedChangesResult.CANCEL

        self._setup_ui()
        self._setup_icon()

    # -- UI setup -------------------------------------------------------------

    def _setup_ui(self) -> None:
        """Build the dialog layout matching MessageDialog structure."""
        self.setObjectName("MessageDialog")
        self.setWindowTitle(self._title_text)
        self.setMinimumWidth(420 + 2 * self._SHADOW_MARGIN)
        self.setMaximumWidth(520 + 2 * self._SHADOW_MARGIN)

        from PySide6.QtGui import QColor
        self._accent_color = QColor(234, 88, 12)

        # Outer layout (transparent)
        dialog_layout = QVBoxLayout(self)
        dialog_layout.setContentsMargins(
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
            self._SHADOW_MARGIN, self._SHADOW_MARGIN,
        )

        # Container frame
        self._container = QFrame()
        self._container.setObjectName("MessageDialog_Container")

        container_layout = QVBoxLayout(self._container)
        container_layout.setSpacing(10)
        container_layout.setContentsMargins(14, 12, 14, 12)

        # -- Header: icon + text ----------------------------------------------
        header_layout = QHBoxLayout()
        header_layout.setSpacing(10)

        self._icon_label = QLabel()
        self._icon_label.setObjectName("MessageDialog_Icon")
        self._icon_label.setFixedSize(28, 28)
        self._icon_label.setScaledContents(False)
        self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        header_layout.addWidget(
            self._icon_label, 0, Qt.AlignmentFlag.AlignTop
        )

        text_layout = QVBoxLayout()
        text_layout.setSpacing(4)

        title_label = QLabel(self._title_text)
        title_label.setObjectName("MessageDialog_Title")
        text_layout.addWidget(title_label)

        message_label = QLabel(self._message_text)
        message_label.setObjectName("MessageDialog_Message")
        message_label.setWordWrap(True)
        text_layout.addWidget(message_label)

        header_layout.addLayout(text_layout, 1)
        container_layout.addLayout(header_layout)

        # -- Buttons ----------------------------------------------------------
        button_container = QWidget()
        button_container.setObjectName("MessageDialog_ButtonContainer")
        button_layout = QHBoxLayout(button_container)
        button_layout.setSpacing(6)
        button_layout.addStretch()

        if self._show_cancel:
            cancel_btn = QPushButton(self._cancel_text)
            cancel_btn.setObjectName("MessageDialog_Button_secondary")
            cancel_btn.clicked.connect(self._on_cancel)
            button_layout.addWidget(cancel_btn)

        discard_btn = QPushButton(self._discard_text)
        discard_btn.setObjectName("MessageDialog_Button_destructive")
        discard_btn.clicked.connect(self._on_discard)
        button_layout.addWidget(discard_btn)

        save_btn = QPushButton(self._save_text)
        save_btn.setObjectName("MessageDialog_Button_primary")
        save_btn.setDefault(True)
        save_btn.clicked.connect(self._on_save)
        button_layout.addWidget(save_btn)

        container_layout.addWidget(button_container)
        dialog_layout.addWidget(self._container)

    def _setup_icon(self) -> None:
        """Set the warning icon."""
        try:
            from src.shared_services.rendering.icons.api import render_svg

            self._icon_label.setPixmap(
                render_svg(Icons.Alert.Warning, size=24)
            )
        except (ImportError, FileNotFoundError):
            pass

    # -- Button handlers ------------------------------------------------------

    @Slot()
    def _on_save(self) -> None:
        self._result = UnsavedChangesResult.SAVE
        self.accept()

    @Slot()
    def _on_discard(self) -> None:
        self._result = UnsavedChangesResult.DISCARD
        self.accept()

    @Slot()
    def _on_cancel(self) -> None:
        self._result = UnsavedChangesResult.CANCEL
        self.reject()

    # -- Result ---------------------------------------------------------------

    def get_result(self) -> UnsavedChangesResult:
        """Return which button the user clicked."""
        return self._result

    # -- Theme ----------------------------------------------------------------

    @Slot(str)
    def _on_theme_changed(self, theme: str) -> None:
        """Update icon when theme changes."""
        self._setup_icon()

    # -- Static convenience ---------------------------------------------------

    @staticmethod
    def ask(
        parent: Optional[QWidget],
        title: str,
        message: str,
        save_text: str = "Speichern",
        discard_text: str = "Verwerfen",
        cancel_text: str = "Abbrechen",
        show_cancel: bool = True,
    ) -> UnsavedChangesResult:
        """Show the dialog and return the user's choice.

        Args:
            parent: Parent widget.
            title: Dialog title.
            message: Message describing the unsaved changes.
            save_text: Label for the save button.
            discard_text: Label for the discard button.
            cancel_text: Label for the cancel button.
            show_cancel: Whether to show the cancel button.

        Returns:
            UnsavedChangesResult.SAVE, .DISCARD, or .CANCEL.
        """
        dialog = UnsavedChangesDialog(
            parent=parent,
            title=title,
            message=message,
            save_text=save_text,
            discard_text=discard_text,
            cancel_text=cancel_text,
            show_cancel=show_cancel,
        )
        dialog.exec()
        return dialog.get_result()
ask(parent, title, message, save_text='Speichern', discard_text='Verwerfen', cancel_text='Abbrechen', show_cancel=True) staticmethod

Show the dialog and return the user's choice.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget.

required
title str

Dialog title.

required
message str

Message describing the unsaved changes.

required
save_text str

Label for the save button.

'Speichern'
discard_text str

Label for the discard button.

'Verwerfen'
cancel_text str

Label for the cancel button.

'Abbrechen'
show_cancel bool

Whether to show the cancel button.

True

Returns:

Type Description
UnsavedChangesResult

UnsavedChangesResult.SAVE, .DISCARD, or .CANCEL.

Source code in src\shared_services\prompt_dialogs\unsaved_changes\unsaved_changes_dialog.py
@staticmethod
def ask(
    parent: Optional[QWidget],
    title: str,
    message: str,
    save_text: str = "Speichern",
    discard_text: str = "Verwerfen",
    cancel_text: str = "Abbrechen",
    show_cancel: bool = True,
) -> UnsavedChangesResult:
    """Show the dialog and return the user's choice.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Message describing the unsaved changes.
        save_text: Label for the save button.
        discard_text: Label for the discard button.
        cancel_text: Label for the cancel button.
        show_cancel: Whether to show the cancel button.

    Returns:
        UnsavedChangesResult.SAVE, .DISCARD, or .CANCEL.
    """
    dialog = UnsavedChangesDialog(
        parent=parent,
        title=title,
        message=message,
        save_text=save_text,
        discard_text=discard_text,
        cancel_text=cancel_text,
        show_cancel=show_cancel,
    )
    dialog.exec()
    return dialog.get_result()
get_result()

Return which button the user clicked.

Source code in src\shared_services\prompt_dialogs\unsaved_changes\unsaved_changes_dialog.py
def get_result(self) -> UnsavedChangesResult:
    """Return which button the user clicked."""
    return self._result

UnsavedChangesResult

Bases: Enum

Result of the unsaved changes dialog.

Source code in src\shared_services\prompt_dialogs\unsaved_changes\unsaved_changes_dialog.py
class UnsavedChangesResult(Enum):
    """Result of the unsaved changes dialog."""

    SAVE = auto()
    DISCARD = auto()
    CANCEL = auto()

ValidationResult dataclass

Result of input validation.

Attributes:

Name Type Description
is_valid bool

Whether the input is valid.

error_message Optional[str]

Error message if invalid, None if valid.

Source code in src\shared_services\prompt_dialogs\input\validators.py
@dataclass
class ValidationResult:
    """
    Result of input validation.

    Attributes:
        is_valid: Whether the input is valid.
        error_message: Error message if invalid, None if valid.
    """

    is_valid: bool
    error_message: Optional[str] = None

apply_sound_patch()

Apply the silent message box patch.

Convenience function for application startup.

Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
def apply_sound_patch() -> None:
    """
    Apply the silent message box patch.

    Convenience function for application startup.
    """
    SoundPatch.apply()

ask_question(parent, title, message, buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)

Show a question dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Question to display.

required
buttons StandardButton

Buttons to show (default: Yes/No).

Yes | No

Returns:

Type Description
StandardButton

The button that was clicked.

Example

Ask a yes/no question::

result = ask_question(self, "Confirm", "Delete this item?")
if result == QMessageBox.StandardButton.Yes:
    delete_item()
Source code in src\shared_services\prompt_dialogs\api.py
def ask_question(
    parent: QWidget | None,
    title: str,
    message: str,
    buttons: QMessageBox.StandardButton = (
        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    ),
) -> QMessageBox.StandardButton:
    """
    Show a question dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Question to display.
        buttons: Buttons to show (default: Yes/No).

    Returns:
        The button that was clicked.

    Example:
        Ask a yes/no question::

            result = ask_question(self, "Confirm", "Delete this item?")
            if result == QMessageBox.StandardButton.Yes:
                delete_item()
    """
    return MessageDialog.question(parent, title, message, buttons)

ask_unsaved_changes(parent, title, message, save_text='Speichern', discard_text='Verwerfen', cancel_text='Abbrechen', show_cancel=True)

Show a themed unsaved-changes dialog with Save / Discard / Cancel.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Message describing the unsaved changes.

required
save_text str

Label for the save button.

'Speichern'
discard_text str

Label for the discard button.

'Verwerfen'
cancel_text str

Label for the cancel button.

'Abbrechen'
show_cancel bool

Whether to show the cancel button.

True

Returns:

Type Description
UnsavedChangesResult

UnsavedChangesResult.SAVE, .DISCARD, or .CANCEL.

Example

Ask with 3 buttons::

result = ask_unsaved_changes(
    self,
    "Ungespeicherte Aenderungen",
    "Aenderungen speichern?",
    save_text="Speichern und Beenden",
    discard_text="Verwerfen und Beenden",
)
if result == UnsavedChangesResult.SAVE:
    save()
elif result == UnsavedChangesResult.CANCEL:
    return
Source code in src\shared_services\prompt_dialogs\api.py
def ask_unsaved_changes(
    parent: QWidget | None,
    title: str,
    message: str,
    save_text: str = "Speichern",
    discard_text: str = "Verwerfen",
    cancel_text: str = "Abbrechen",
    show_cancel: bool = True,
) -> UnsavedChangesResult:
    """
    Show a themed unsaved-changes dialog with Save / Discard / Cancel.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Message describing the unsaved changes.
        save_text: Label for the save button.
        discard_text: Label for the discard button.
        cancel_text: Label for the cancel button.
        show_cancel: Whether to show the cancel button.

    Returns:
        UnsavedChangesResult.SAVE, .DISCARD, or .CANCEL.

    Example:
        Ask with 3 buttons::

            result = ask_unsaved_changes(
                self,
                "Ungespeicherte Aenderungen",
                "Aenderungen speichern?",
                save_text="Speichern und Beenden",
                discard_text="Verwerfen und Beenden",
            )
            if result == UnsavedChangesResult.SAVE:
                save()
            elif result == UnsavedChangesResult.CANCEL:
                return
    """
    return UnsavedChangesDialog.ask(
        parent, title, message,
        save_text=save_text,
        discard_text=discard_text,
        cancel_text=cancel_text,
        show_cancel=show_cancel,
    )

get_recent_interceptions(count=10)

Get recent interception entries.

Convenience function for retrieving entries.

Parameters:

Name Type Description Default
count int

Maximum entries to return.

10

Returns:

Type Description
List[InterceptionEntry]

List of InterceptionEntry objects.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
def get_recent_interceptions(count: int = 10) -> List[InterceptionEntry]:
    """
    Get recent interception entries.

    Convenience function for retrieving entries.

    Args:
        count: Maximum entries to return.

    Returns:
        List of InterceptionEntry objects.
    """
    return InterceptionLog.get_recent_entries(count)

log_interception(title, message, message_type=MessageBoxType.UNKNOWN, capture_stack=True)

Log an intercepted message box.

Convenience function for logging interceptions.

Parameters:

Name Type Description Default
title str

Message box title.

required
message str

Message box content.

required
message_type MessageBoxType

Type of message box.

UNKNOWN
capture_stack bool

Whether to capture call stack.

True

Returns:

Type Description
InterceptionEntry

The created InterceptionEntry.

Source code in src\shared_services\prompt_dialogs\interception\interception_log.py
def log_interception(
    title: str,
    message: str,
    message_type: MessageBoxType = MessageBoxType.UNKNOWN,
    capture_stack: bool = True,
) -> InterceptionEntry:
    """
    Log an intercepted message box.

    Convenience function for logging interceptions.

    Args:
        title: Message box title.
        message: Message box content.
        message_type: Type of message box.
        capture_stack: Whether to capture call stack.

    Returns:
        The created InterceptionEntry.
    """
    return InterceptionLog.log(
        message_type=message_type,
        title=title,
        message=message,
        capture_stack=capture_stack,
    )

refresh_sound_patch_for_theme()

Refresh the sound patch icons for a theme change.

Call this when the application theme changes.

Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
def refresh_sound_patch_for_theme() -> None:
    """
    Refresh the sound patch icons for a theme change.

    Call this when the application theme changes.
    """
    SoundPatch.refresh_for_theme()

remove_sound_patch()

Remove the silent message box patch.

Convenience function for testing.

Source code in src\shared_services\prompt_dialogs\sound\sound_patch.py
def remove_sound_patch() -> None:
    """
    Remove the silent message box patch.

    Convenience function for testing.
    """
    SoundPatch.remove()

show_error(parent, title, message, buttons=QMessageBox.StandardButton.Ok)

Show an error message dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Error message to display.

required
buttons StandardButton

Buttons to show.

Ok

Returns:

Type Description
StandardButton

The button that was clicked.

Example

Show an error message::

show_error(self, "Error", "Failed to save file.")
Source code in src\shared_services\prompt_dialogs\api.py
def show_error(
    parent: QWidget | None,
    title: str,
    message: str,
    buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
) -> QMessageBox.StandardButton:
    """
    Show an error message dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Error message to display.
        buttons: Buttons to show.

    Returns:
        The button that was clicked.

    Example:
        Show an error message::

            show_error(self, "Error", "Failed to save file.")
    """
    return MessageDialog.error(parent, title, message, buttons)

show_info(parent, title, message, buttons=QMessageBox.StandardButton.Ok)

Show an info message dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Message to display.

required
buttons StandardButton

Buttons to show.

Ok

Returns:

Type Description
StandardButton

The button that was clicked.

Example

Show a simple info message::

show_info(self, "Success", "File saved successfully.")
Source code in src\shared_services\prompt_dialogs\api.py
def show_info(
    parent: QWidget | None,
    title: str,
    message: str,
    buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
) -> QMessageBox.StandardButton:
    """
    Show an info message dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Message to display.
        buttons: Buttons to show.

    Returns:
        The button that was clicked.

    Example:
        Show a simple info message::

            show_info(self, "Success", "File saved successfully.")
    """
    return MessageDialog.info(parent, title, message, buttons)

show_warning(parent, title, message, buttons=QMessageBox.StandardButton.Ok)

Show a warning message dialog.

Parameters:

Name Type Description Default
parent QWidget | None

Parent widget.

required
title str

Dialog title.

required
message str

Warning message to display.

required
buttons StandardButton

Buttons to show.

Ok

Returns:

Type Description
StandardButton

The button that was clicked.

Example

Show a warning with Yes/No buttons::

result = show_warning(
    self,
    "Warning",
    "Are you sure you want to proceed?",
    buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if result == QMessageBox.StandardButton.Yes:
    proceed()
Source code in src\shared_services\prompt_dialogs\api.py
def show_warning(
    parent: QWidget | None,
    title: str,
    message: str,
    buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
) -> QMessageBox.StandardButton:
    """
    Show a warning message dialog.

    Args:
        parent: Parent widget.
        title: Dialog title.
        message: Warning message to display.
        buttons: Buttons to show.

    Returns:
        The button that was clicked.

    Example:
        Show a warning with Yes/No buttons::

            result = show_warning(
                self,
                "Warning",
                "Are you sure you want to proceed?",
                buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            )
            if result == QMessageBox.StandardButton.Yes:
                proceed()
    """
    return MessageDialog.warning(parent, title, message, buttons)

validate_file_name(name, blacklist=None)

Validate a file name.

Convenience function for simple file name validation.

Parameters:

Name Type Description Default
name str

The file name to validate.

required
blacklist List[str] | None

Optional list of disallowed names.

None

Returns:

Type Description
Tuple[bool, str]

Tuple of (is_valid, error_message). Error message is empty if valid.

Source code in src\shared_services\prompt_dialogs\input\validators.py
def validate_file_name(name: str, blacklist: List[str] | None = None) -> Tuple[bool, str]:
    """
    Validate a file name.

    Convenience function for simple file name validation.

    Args:
        name: The file name to validate.
        blacklist: Optional list of disallowed names.

    Returns:
        Tuple of (is_valid, error_message). Error message is empty if valid.
    """
    validator = InputValidator(blacklist=blacklist)
    result = validator.validate(name)
    return result.is_valid, result.error_message or ""