Source code for betty.gui

"""Provide the Graphical User Interface (GUI) for Betty Desktop."""

from __future__ import annotations

import pickle
from contextlib import asynccontextmanager
from typing import Any, TypeVar, Self, TYPE_CHECKING

from PyQt6.QtCore import pyqtSlot, QObject, QCoreApplication
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QApplication, QWidget
from typing_extensions import override

from betty.app import App
from betty.gui.error import ExceptionError, _UnexpectedExceptionError
from betty.locale import Str
from betty.serde.format import FormatRepository

if TYPE_CHECKING:
    from collections.abc import AsyncIterator

QWidgetT = TypeVar("QWidgetT", bound=QWidget)


[docs] def get_configuration_file_filter() -> Str: """ Get the Qt file filter for project configuration files. """ serde_formats = FormatRepository() return Str._( "Betty project configuration ({supported_formats})", supported_formats=" ".join( f"*.{extension}" for serde_format in serde_formats.formats for extension in serde_format.extensions ), )
[docs] class GuiBuilder: """ Allow extensions to provide their own Graphical User Interface component. """
[docs] def gui_build(self) -> QWidget: """ Build this extension's Graphical User Interface component. """ raise NotImplementedError(repr(self))
[docs] def mark_valid(widget: QWidget) -> None: """ Mark a widget as currently containing valid input. """ widget.setProperty("invalid", "false") widget.setStyle(widget.style()) widget.setToolTip("")
[docs] def mark_invalid(widget: QWidget, reason: str) -> None: """ Mark a widget as currently containing invalid input. """ widget.setProperty("invalid", "true") widget.setStyle(widget.style()) widget.setToolTip(reason)
[docs] class BettyApplication(QApplication): """ A Betty Qt application. """
[docs] def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self._app: App | None = None self.setApplicationName("Betty") self.setStyleSheet(self._stylesheet())
def _is_dark_mode(self) -> bool: palette = self.palette() window_lightness = palette.color(QPalette.ColorRole.Window).lightness() window_text_lightness = palette.color(QPalette.ColorRole.WindowText).lightness() return window_lightness < window_text_lightness def _stylesheet(self) -> str: caption_color = "#eeeeee" if self._is_dark_mode() else "#333333" return f""" Caption {{ color: {caption_color}; font-size: 14px; margin-bottom: 0.3em; }} Code {{ font-family: monospace; }} QLineEdit[invalid="true"] {{ border: 1px solid red; color: red; }} QPushButton[pane-selector="true"] {{ padding: 10px; }} LogRecord[level="50"], LogRecord[level="40"] {{ color: red; }} LogRecord[level="30"] {{ color: yellow; }} LogRecord[level="20"] {{ color: green; }} LogRecord[level="10"], LogRecord[level="0"] {{ color: white; }} _WelcomeText {{ padding: 10px; }} _WelcomeTitle {{ font-size: 20px; padding: 10px; }} _WelcomeHeading {{ font-size: 16px; margin-top: 50px; }} _WelcomeAction {{ padding: 10px; }} """ @pyqtSlot( type, bytes, QObject, bool, ) def _show_user_facing_error( self, error_type: type[Exception], pickled_error_message: bytes, parent: QObject, close_parent: bool, ) -> None: error_message = pickle.loads(pickled_error_message) window = ExceptionError( self.app, error_message, error_type, parent=parent, close_parent=close_parent, ) window.show() @pyqtSlot( type, str, str, QObject, bool, ) def _show_unexpected_exception( self, error_type: type[Exception], error_message: str, error_traceback: str, parent: QObject, close_parent: bool, ) -> None: window = _UnexpectedExceptionError( self.app, error_type, error_message, error_traceback, parent=parent, close_parent=close_parent, ) window.show()
[docs] @override @classmethod def instance(cls) -> Self: qapp = QCoreApplication.instance() assert isinstance(qapp, cls) return qapp
[docs] @asynccontextmanager async def with_app(self, app: App) -> AsyncIterator[Self]: """ Temporarily set assign a Betty application to this Qt application. """ if self._app is not None: raise RuntimeError(f"This {type(self)} already has an {App}.") self._app = app yield self self._app = None
@property def app(self) -> App: """ Get this Qt application's Betty application. """ if self._app is None: raise RuntimeError(f"This {type(self)} does not have an {App} yet.") return self._app