Source code for pymodaq_gui.utils.status_palette

"""
PyMoDAQ Status Color Palette
=============================

Proposal for a community-wide color convention for status indicators
(LEDs, icons, status bars) across all PyMoDAQ modules and plugins.

Rationale
---------
Instrument control software exposes users to many concurrent status
indicators.  When each component chooses its own colors the user must
re-learn the meaning of each indicator.  A shared vocabulary lets users
read the state of the system at a glance.

Colors are resolved from the active ``qt_themes`` theme so that they
adapt to the user's chosen dark or light theme.  Hex fallbacks are
provided for headless contexts or when a theme attribute is absent.

The six states cover the full lifecycle of a PyMoDAQ module or
operation.  The upper three states (warning / error / critical) align
deliberately with Python's ``logging`` severity levels so that the
visual vocabulary is familiar to developers.

Color Definitions
-----------------

+------------------+--------+----------------------+----------------------+
| State            | Color  | logging analogy      | Meaning              |
+==================+========+======================+======================+
| ``off``          | grey   | —                    | Module absent, not   |
|                  |        |                      | initialized, or      |
|                  |        |                      | hardware not yet     |
|                  |        |                      | connected.           |
+------------------+--------+----------------------+----------------------+
| ``idle``         | green  | —                    | Initialized and      |
|                  |        |                      | ready — waiting for  |
|                  |        |                      | a command or trigger.|
+------------------+--------+----------------------+----------------------+
| ``running``      | blue   | —                    | A command is in      |
|                  |        |                      | flight: moving,      |
|                  |        |                      | acquiring, or        |
|                  |        |                      | processing data.     |
|                  |        |                      | Hex fallback: blue   |
|                  |        |                      | ``#0078d4``.         |
+------------------+--------+----------------------+----------------------+
| ``warning``      | yellow | ``logging.WARNING``  | A non-fatal issue.   |
|                  |        |                      | Still functional;    |
|                  |        |                      | user attention       |
|                  |        |                      | advised.  Fallback:  |
|                  |        |                      | amber ``#ccaa00``.   |
+------------------+--------+----------------------+----------------------+
| ``error``        | orange | ``logging.ERROR``    | An operation failed. |
|                  |        |                      | Module may still     |
|                  |        |                      | recover. Fallback:   |
|                  |        |                      | ``#dc6400``.         |
+------------------+--------+----------------------+----------------------+
| ``critical``     | red    | ``logging.CRITICAL`` | Unrecoverable fault. |
|                  |        |                      | Timeout, hardware    |
|                  |        |                      | error, or fatal      |
|                  |        |                      | exception.           |
+------------------+--------+----------------------+----------------------+

Usage with MultistateLED
------------------------

.. code-block:: python

    from pymodaq_gui.utils.status_palette import StatusPalette
    from pymodaq_gui.utils.widgets.multistate_led import MultistateLED

    # Full six-state indicator
    led = MultistateLED(states=StatusPalette.as_states())

    # Subset — e.g. a connection indicator without 'warning'
    led = MultistateLED(states=StatusPalette.subset('off', 'idle', 'error'))
    # ... or, equivalently, using the Status StrEnum
    led = MultistateLED(states=StatusPalette.subset(Status.OFF, Status.IDLE, Status.ERROR))

Usage in a parameter tree
-------------------------

.. code-block:: python

    from pymodaq_gui.utils.status_palette import StatusPalette

    params = [
        {'name': 'acq_status', 'type': 'action_multistate_led',
         'value': 'off',
         'states': StatusPalette.as_states()},
    ]
"""

from __future__ import annotations

import qt_themes
from qtpy import QtGui

from pymodaq_utils.enums import StrEnum


[docs] class Status(StrEnum): """The six canonical PyMoDAQ status states (see module docstring). Members behave as plain ``str`` (e.g. ``Status.IDLE == 'idle'``), so they can be passed anywhere a state-name string is expected — including :meth:`StatusPalette.subset` and :class:`~pymodaq_gui.utils.widgets.multistate_led.MultistateLED`. """ OFF = 'off' IDLE = 'idle' RUNNING = 'running' WARNING = 'warning' ERROR = 'error' CRITICAL = 'critical'
# (state_name, theme_attribute, hex_fallback) # theme_attribute is the name of the QColor property on a qt_themes Theme object. _DEFINITIONS: list[tuple[Status, str, str]] = [ (Status.OFF, 'grey', '#808080'), # grey — adapts to dark/light theme (Status.IDLE, 'green', '#00b400'), # green — initialized and ready (Status.RUNNING, 'blue', '#0078d4'), # blue — action in flight (Status.WARNING, 'yellow', '#ccaa00'), # amber — non-fatal issue (Status.ERROR, 'orange', '#dc6400'), # orange — operation failed, may recover (Status.CRITICAL, 'red', '#c80000'), # red — unrecoverable fault ] def _resolve(theme_attr: str, fallback: str) -> QtGui.QColor: """Return the theme QColor for *theme_attr*, or *fallback* hex if absent.""" try: color = getattr(qt_themes.get_theme(), theme_attr, None) if isinstance(color, QtGui.QColor) and color.isValid(): return color except Exception: pass return QtGui.QColor(fallback)
[docs] class StatusPalette: """Standard status color definitions for PyMoDAQ. Colors are drawn from the active ``qt_themes`` theme so they adapt to dark / light modes. Each entry resolves to a ``(name, QColor)`` pair compatible with :class:`~pymodaq_gui.utils.widgets.multistate_led.MultistateLED`. The states are ordered from *least active* to *most severe*. """
[docs] @classmethod def as_states(cls) -> list[tuple[str, QtGui.QColor]]: """Return all five states with theme-resolved colors.""" return [(name, _resolve(attr, fb)) for name, attr, fb in _DEFINITIONS]
[docs] @classmethod def subset(cls, *names: str) -> list[tuple[str, QtGui.QColor]]: """Return a subset of states in canonical order with theme-resolved colors. Parameters ---------- *names: State names to include: ``'off'``, ``'idle'``, ``'running'``, ``'warning'``, ``'error'``. Raises ------ ValueError If an unknown name is requested. Example ------- >>> StatusPalette.subset('off', 'idle', 'error') [('off', QColor(...)), ('idle', QColor(...)), ('error', QColor(...))] """ known = {name: (attr, fb) for name, attr, fb in _DEFINITIONS} unknown = set(names) - known.keys() if unknown: raise ValueError( f"Unknown state(s): {sorted(unknown)}. " f"Valid states: {list(known)}" ) return [(n, _resolve(*known[n])) for n in names if n in known]
[docs] @classmethod def color(cls, name: str) -> QtGui.QColor: """Return the theme-resolved QColor for a single state name. Useful when you need just the color, e.g. for an icon or stylesheet. """ for n, attr, fb in _DEFINITIONS: if n == name: return _resolve(attr, fb) raise ValueError( f"Unknown state {name!r}. Valid states: {[n for n, _, _ in _DEFINITIONS]}" )