Skip to content

visualdynamics.gui

gui

Desktop GUI. Start with visualdynamics-gui [files...] or python -m visualdynamics.

Functions:

Name Description
qt_binding

Which Qt pyqtgraph is bound to, or None if it is still free.

check_qt_binding

Point pyqtgraph at the same Qt as the rest of the GUI, or explain.

theme_flag

Take --theme dark, --theme light or --theme=dark out of

Functions:

qt_binding

qt_binding() -> str | None

Which Qt pyqtgraph is bound to, or None if it is still free.

None means nothing has imported pyqtgraph yet, so importing visualdynamics's own widgets first will decide it in our favor.

Source code in src/visualdynamics/gui/__init__.py
def qt_binding() -> str | None:
    """Which Qt pyqtgraph is bound to, or None if it is still free.

    None means nothing has imported pyqtgraph yet, so importing visualdynamics's
    own widgets first will decide it in our favor.
    """
    import sys

    module = sys.modules.get('pyqtgraph')
    return None if module is None else module.Qt.QT_LIB

check_qt_binding

check_qt_binding() -> None

Point pyqtgraph at the same Qt as the rest of the GUI, or explain.

pyqtgraph binds to one Qt for the life of the process, chosen when it is first imported: whichever binding is already in sys.modules, and failing that its own order of preference, which puts PyQt6 ahead of PySide6. Having PySide6 installed is not enough.

visualdynamics is a PySide6 application, so a pyqtgraph on any other binding hands back plot widgets a PySide6 layout will not accept. That surfaced as QSplitter.addWidget called with wrong argument types from deep inside the main window, naming nothing useful.

So: import PySide6 first, which decides it in our favor whenever pyqtgraph has not yet been imported. When it has — sdynpy imports it with PyQt5 at import sdynpy — the choice cannot be undone, and all that is left is to say so before a window is built out of the mismatch.

Source code in src/visualdynamics/gui/__init__.py
def check_qt_binding() -> None:
    """Point pyqtgraph at the same Qt as the rest of the GUI, or explain.

    pyqtgraph binds to one Qt for the life of the process, chosen when it is
    first imported: whichever binding is already in `sys.modules`, and
    failing that its own order of preference, which puts **PyQt6 ahead of
    PySide6**. Having PySide6 installed is not enough.

    visualdynamics is a PySide6 application, so a pyqtgraph on any other binding hands
    back plot widgets a PySide6 layout will not accept. That surfaced as
    `QSplitter.addWidget called with wrong argument types` from deep inside
    the main window, naming nothing useful.

    So: import PySide6 first, which decides it in our favor whenever
    pyqtgraph has not yet been imported. When it has — sdynpy imports it
    with PyQt5 at `import sdynpy` — the choice cannot be undone, and all
    that is left is to say so before a window is built out of the mismatch.
    """
    import importlib

    import PySide6.QtWidgets  # noqa: F401  — imported for its side effect

    # deliberately not an import statement: these two are ordered, and an
    # import sorter would put pyqtgraph first and undo the whole point
    binding = importlib.import_module('pyqtgraph').Qt.QT_LIB
    if binding == 'PySide6':
        return
    raise RuntimeError(
        f'pyqtgraph is using {binding}, but visualdynamics is a PySide6 application, '
        f'and widgets from two Qt bindings cannot share a window.\n'
        f'\n'
        f'pyqtgraph picks its binding when it is first imported, and '
        f'something imported it before visualdynamics — sdynpy does this, with PyQt5, '
        f'at "import sdynpy". The choice cannot be undone in a running '
        f'process.\n'
        f'\n'
        f'`visualdynamics.launch_gui()` handles this by starting the app in a '
        f'fresh process; this path is for running the window inside the '
        f'current one, which needs an interpreter that has not imported '
        f'sdynpy — or PYQTGRAPH_QT_LIB=PySide6 set before any import, '
        f'which then breaks sdynpy\'s own plotting in that process.')

theme_flag

theme_flag(argv: list[str]) -> tuple[list[str], str | None]

Take --theme dark, --theme light or --theme=dark out of the arguments: the rest, and the theme named, or None.

Source code in src/visualdynamics/gui/__init__.py
def theme_flag(argv: list[str]) -> tuple[list[str], str | None]:
    """Take `--theme dark`, `--theme light` or `--theme=dark` out of
    the arguments: the rest, and the theme named, or None."""
    rest: list[str] = []
    theme = None
    skip = False
    for i, arg in enumerate(argv):
        if skip:
            skip = False
            continue
        if arg.startswith('--theme='):
            theme = arg.split('=', 1)[1]
        elif arg == '--theme' and i + 1 < len(argv):
            theme = argv[i + 1]
            skip = True
        else:
            rest.append(arg)
    if theme is not None:
        theme = theme.strip().lower()
        if theme not in ('dark', 'light'):
            raise SystemExit(f"--theme takes dark or light, not {theme!r}")
    return rest, theme