Files
bec_widgets/bec_widgets/utils/scan_arg_metadata.py
T

156 lines
5.2 KiB
Python

from __future__ import annotations
import re
from collections.abc import Mapping
from typing import Any
from bec_lib import bec_logger
from qtpy.QtWidgets import QDoubleSpinBox, QSpinBox, QWidget
logger = bec_logger.logger
UNIT_TOOLTIP_PREFIXES = ("Units:", "Units from:")
def format_display_name(name: str) -> str:
"""Convert a raw argument name into a user-facing label."""
parts = re.split(r"(_|\d+)", name)
return " ".join(part.capitalize() for part in parts if part.isalnum()).strip()
def resolve_tooltip(scan_argument: Mapping[str, Any]) -> str | None:
"""Resolve explicit tooltip text, falling back to the description."""
return scan_argument.get("tooltip") or scan_argument.get("description")
def ui_config_from_metadata(
name: str,
metadata: Mapping[str, Any],
*,
default: Any = None,
input_type: Any = None,
arg: bool = False,
display_name: str | None = None,
) -> dict[str, Any]:
"""Build the normalized scan-input item consumed by form widgets."""
return {
"arg": arg,
"name": name,
"type": input_type,
"display_name": display_name or metadata.get("display_name") or format_display_name(name),
"tooltip": resolve_tooltip(metadata),
"default": default,
"expert": metadata.get("expert", False),
"hidden": metadata.get("hidden", False),
"precision": metadata.get("precision"),
"units": metadata.get("units"),
"reference_units": metadata.get("reference_units"),
"reference_limits": metadata.get("reference_limits"),
"gt": metadata.get("gt"),
"ge": metadata.get("ge"),
"lt": metadata.get("lt"),
"le": metadata.get("le"),
"alternative_group": metadata.get("alternative_group"),
}
def unit_tooltip(item: Mapping[str, Any], units: str | None = None) -> str | None:
"""Build tooltip text from scan argument unit metadata."""
tooltip = item.get("tooltip")
reference_units = item.get("reference_units")
units = units or item.get("units")
tooltip_parts = [tooltip] if tooltip else []
if units:
tooltip_parts.append(f"Units: {units}")
elif reference_units:
tooltip_parts.append(f"Units from: {reference_units}")
if tooltip_parts:
return "\n".join(str(part) for part in tooltip_parts)
return None
def strip_unit_tooltip(tooltip: str) -> str:
"""Remove unit lines added by :func:`apply_unit_metadata`."""
return "\n".join(
line for line in tooltip.splitlines() if not line.startswith(UNIT_TOOLTIP_PREFIXES)
).strip()
def apply_unit_metadata(widget: QWidget, item: Mapping[str, Any], units: str | None = None) -> None:
"""Apply unit tooltip text and numeric suffix metadata to a widget."""
units = units or item.get("units")
tooltip = unit_tooltip(item, units)
existing_tooltip = strip_unit_tooltip(widget.toolTip())
base_tooltip = item.get("tooltip")
if base_tooltip and existing_tooltip == base_tooltip:
existing_tooltip = ""
if tooltip:
widget.setToolTip(f"{existing_tooltip}\n{tooltip}" if existing_tooltip else tooltip)
else:
widget.setToolTip(existing_tooltip)
if hasattr(widget, "setSuffix"):
widget.setSuffix(f" {units}" if units else "")
def device_units(device: object) -> str | None:
"""Return engineering units from a BEC device object when available."""
egu = getattr(device, "egu", None)
if not callable(egu):
return None
try:
return egu()
except Exception:
logger.exception("Failed to fetch engineering units from device %s", device)
return None
def apply_numeric_precision(widget: QWidget, item: Mapping[str, Any]) -> None:
"""Apply decimal precision metadata to spinboxes supporting ``setDecimals``."""
if not hasattr(widget, "setDecimals"):
return
precision = item.get("precision")
if precision is None:
return
try:
widget.setDecimals(max(0, int(precision)))
except (TypeError, ValueError):
logger.warning(
"Ignoring invalid precision %r for parameter %s", precision, item.get("name")
)
def apply_numeric_limits(widget: QWidget, item: Mapping[str, Any]) -> None:
"""Apply ``gt/ge/lt/le`` numeric bounds to Qt spinboxes."""
if isinstance(widget, QSpinBox) and not isinstance(widget, QDoubleSpinBox):
minimum = -2147483647
maximum = 2147483647
if item.get("ge") is not None:
minimum = int(item["ge"])
if item.get("gt") is not None:
minimum = int(item["gt"]) + 1
if item.get("le") is not None:
maximum = int(item["le"])
if item.get("lt") is not None:
maximum = int(item["lt"]) - 1
widget.setRange(minimum, maximum)
return
if isinstance(widget, QDoubleSpinBox):
minimum = -float("inf")
maximum = float("inf")
step = 10 ** (-widget.decimals())
if item.get("ge") is not None:
minimum = float(item["ge"])
if item.get("gt") is not None:
minimum = float(item["gt"]) + step
if item.get("le") is not None:
maximum = float(item["le"])
if item.get("lt") is not None:
maximum = float(item["lt"]) - step
widget.setRange(minimum, maximum)