refactor(digital-twin): compact widget layout

Wrap side panels in vertical scroll areas, tighten row and group spacing, use a balanced 1:1 plot area, and avoid assigning a second top-level layout.

Also aligns MotionWorker signal signatures with the slots connected to them as part of the widget cleanup.
This commit is contained in:
2026-06-09 15:42:26 +02:00
parent 6bce6f8907
commit dc6966ee31
7 changed files with 102 additions and 150 deletions
@@ -22,10 +22,13 @@ from qtpy.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QFrame,
QHBoxLayout,
QLabel,
QPlainTextEdit,
QPushButton,
QScrollArea,
QSizePolicy,
QStyle,
QVBoxLayout,
QWidget,
@@ -74,32 +77,47 @@ class DigitalTwin(BECWidget, QWidget):
self.check_config()
self.bec_dispatcher.connect_slot(self.check_config, MessageEndpoints.device_config_update())
central = QWidget()
self.root_layout = QHBoxLayout(central)
self.content_widget = QWidget(self)
self.root_layout = QHBoxLayout(self.content_widget)
self.root_layout.setContentsMargins(6, 6, 6, 6)
self.root_layout.setSpacing(6)
self.input_widget = QWidget()
self.input_layout = QVBoxLayout(self.input_widget)
self.input_layout.setContentsMargins(4, 4, 4, 4)
self.input_layout.setSpacing(6)
self.input = InputPanel()
self.settings = SettingsPanel()
self.input_layout.addWidget(self.input)
self.input_layout.addWidget(self.settings)
self.input_layout.addStretch()
self.plot_widget = QWidget()
self.plot_layout = QVBoxLayout(self.plot_widget)
self.plot_layout.setContentsMargins(4, 4, 4, 4)
self.plot_layout.setSpacing(6)
self.sideview_plot = SideviewPlot()
self.surface_plots = SurfacePlots()
self.plot_layout.addWidget(self.sideview_plot)
self.plot_layout.addWidget(self.surface_plots)
self.plot_layout.addWidget(self.sideview_plot, stretch=1)
self.plot_layout.addWidget(self.surface_plots, stretch=1)
self.plot_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.mover = MoverPanel(self.dev)
self.root_layout.addWidget(self.input_widget, alignment=Qt.AlignmentFlag.AlignTop)
self.root_layout.addWidget(self.plot_widget, alignment=Qt.AlignmentFlag.AlignTop)
self.root_layout.addWidget(self.mover, alignment=Qt.AlignmentFlag.AlignTop)
self.input_scroll = self._scroll_area(self.input_widget, min_width=320, max_width=360)
self.mover_scroll = self._scroll_area(self.mover, min_width=380, max_width=460)
self.setLayout(self.root_layout)
self.root_layout.addWidget(self.input_scroll)
self.root_layout.addWidget(self.plot_widget, stretch=1)
self.root_layout.addWidget(self.mover_scroll)
widget_layout = self.layout()
if widget_layout is None:
widget_layout = QVBoxLayout(self)
widget_layout.setContentsMargins(0, 0, 0, 0)
widget_layout.setSpacing(0)
widget_layout.addWidget(self.content_widget)
self.setWindowTitle("Digital Twin")
self.resize(1800, 800)
self.resize(1450, 760)
self.input.energy.value_changed_connect(self.calc_assistant)
self.input.sldi_hacc.value_changed_connect(self.calc_assistant)
@@ -127,12 +145,26 @@ class DigitalTwin(BECWidget, QWidget):
self.load_offsets(recalculate=False)
self.calc_assistant(identifier="init")
# Timer: update plots every 1 second
# Timer: update reality plots every 1 second
self._timer = QTimer(self)
self._timer.setInterval(100)
self._timer.setInterval(1000)
self._timer.timeout.connect(self.calc_reality)
self._timer.start()
@staticmethod
def _scroll_area(widget: QWidget, min_width: int, max_width: int) -> QScrollArea:
"""Wrap a side panel in a compact vertical scroll area."""
scroll = QScrollArea()
scroll.setWidgetResizable(True)
widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding)
scroll.setWidget(widget)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
scroll.setMinimumWidth(min_width)
scroll.setMaximumWidth(max_width)
scroll.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
return scroll
def apply_theme(self, theme: Literal["dark", "light"]):
"""
Apply the theme
@@ -152,8 +184,8 @@ class DigitalTwin(BECWidget, QWidget):
BEC dispatcher whenever there is a config update, stop the timer
that updates the plot in the background.
"""
reload = (args[0] if args else {}).get("action") == "reload"
if reload:
reload_config = (args[0] if args else {}).get("action") == "reload"
if reload_config:
self._timer.stop()
devices = [
"abs",
@@ -234,7 +266,7 @@ class DigitalTwin(BECWidget, QWidget):
running_app = QApplication.instance()
if running_app is not None:
running_app.exit(0)
if reload:
if reload_config:
self._timer.start()
@SafeSlot()
@@ -3,7 +3,7 @@ Panel for user inputs of the digital twin widget
"""
# pylint: disable=E0611
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
from qtpy.QtWidgets import QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
Button,
@@ -20,7 +20,8 @@ class InputPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self._layout.setContentsMargins(4, 4, 4, 4)
self._layout.setSpacing(4)
# Adapt to reality
self.adapt_reality = Button(label_button="Adapt to reality", enabled=True)
@@ -5,7 +5,7 @@ Panel to move an axis to a certain position
from typing import Literal
# pylint: disable=E0611
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
from qtpy.QtWidgets import QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.widgets.move_widget import (
AbsorberWidget,
@@ -20,7 +20,8 @@ class MoverPanel(QWidget):
def __init__(self, dev, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self._layout.setContentsMargins(4, 4, 4, 4)
self._layout.setSpacing(4)
self.mover_widgets = []
@@ -189,7 +190,7 @@ class MoverPanel(QWidget):
)
self.mover_widgets.append(self.es0wi_try)
self.es0_mov_group = Group("Expperimental Station 0", [self.es0wi_try])
self.es0_mov_group = Group("Experimental Station 0", [self.es0wi_try])
# Experimental Station 1
self.ot_es1_trz = MoveWidget(
@@ -197,7 +198,7 @@ class MoverPanel(QWidget):
)
self.mover_widgets.append(self.ot_es1_trz)
self.es1_mov_group = Group("Expperimental Station 1", [self.ot_es1_trz])
self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz])
# Assemble complete mover group
self.mover_group = Group(
@@ -33,6 +33,8 @@ class SurfacePlots(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self._layout = QHBoxLayout(self)
self._layout.setContentsMargins(4, 4, 4, 4)
self._layout.setSpacing(6)
self.surfaces: dict[str, SurfaceDict] = {
"assistant": {
@@ -202,7 +204,8 @@ class SideviewPlot(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self._layout = QVBoxLayout(self)
# self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self._layout.setContentsMargins(4, 4, 4, 4)
self._layout.setSpacing(0)
self.plot_widget = pg.PlotWidget()
self.plot_widget.getAxis("bottom").enableAutoSIPrefix(False)
@@ -243,7 +246,6 @@ class SideviewPlot(QWidget):
self.plot_widget.hideButtons()
self._layout.addWidget(self.plot_group)
self._layout.addStretch()
self.plot_vacuum_pipes()
self.plot_walls()
@@ -3,7 +3,7 @@ Settings panel for the digital twin widget
"""
# pylint: disable=E0611
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
from qtpy.QtWidgets import QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
Button,
@@ -18,7 +18,8 @@ class SettingsPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self._layout.setContentsMargins(4, 4, 4, 4)
self._layout.setSpacing(4)
# Reload offsets
self.load_offsets = Button(label="Load Offsets", label_button="Load", enabled=True)
@@ -128,8 +128,8 @@ class MotionWorker(QObject):
"""
position_changed = Signal(float)
error = Signal(bool) # True = error
finished = Signal(bool) # True = reached target, False = stopped
error = Signal()
finished = Signal()
def __init__(self, dev, motor, target_pos: float):
super().__init__()
@@ -284,7 +284,7 @@ class MotionWorker(QObject):
fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]:
self.dev[self.motor].stop()
self.error.emit(1)
self.error.emit()
break
self.finished.emit()
@@ -315,35 +315,33 @@ class MoveWidget(QWidget):
self.decimals = decimals
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
layout.setContentsMargins(4, 0, 4, 0)
layout.setSpacing(4)
# Name
self.label = QLabel(label)
self.label.setFixedWidth(100)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setFixedWidth(76)
self.label.setWordWrap(True)
layout.addWidget(self.label)
# Target
self.target_label = QLabel("-")
self.target_label.setFixedWidth(100)
self.target_label.setFixedWidth(84)
layout.addWidget(self.target_label)
# Feedback
self.fb_label = QLabel("-")
self.fb_label.setFixedWidth(100)
self.fb_label.setFixedWidth(84)
layout.addWidget(self.fb_label)
# Status icon
self.status_icon = StatusIcon()
self.status_icon.setFixedWidth(30)
self.status_icon.setContentsMargins(0, 0, 10, 0)
self.status_icon.setFixedWidth(24)
layout.addWidget(self.status_icon)
# Start / Stop button
self.btn_action = QPushButton("Move")
self.btn_action.setFixedWidth(90)
self.btn_action.setFixedWidth(64)
self.btn_action.setFixedHeight(20)
self.btn_action.clicked.connect(self._on_button_clicked)
layout.addWidget(self.btn_action)
@@ -522,35 +520,33 @@ class AbsorberWidget(QWidget):
self.text_color = (0, 0, 0)
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
layout.setContentsMargins(4, 0, 4, 0)
layout.setSpacing(4)
# Name
self.label = QLabel(label)
self.label.setFixedWidth(100)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setFixedWidth(76)
self.label.setWordWrap(True)
layout.addWidget(self.label)
# Blank
self.blank_label = QLabel("")
self.blank_label.setFixedWidth(100)
self.blank_label.setFixedWidth(84)
layout.addWidget(self.blank_label)
# Feedback
self.fb_label = QLabel("-")
self.fb_label.setFixedWidth(100)
self.fb_label.setFixedWidth(84)
layout.addWidget(self.fb_label)
# Blank icon
self.blank_icon = QLabel("")
self.blank_icon.setFixedWidth(30)
self.blank_icon.setContentsMargins(0, 0, 10, 0)
self.blank_icon.setFixedWidth(24)
layout.addWidget(self.blank_icon)
# Open
self.btn_action = QPushButton("Open")
self.btn_action.setFixedWidth(90)
self.btn_action.setFixedWidth(64)
self.btn_action.setFixedHeight(20)
self.btn_action.clicked.connect(self._on_button_clicked)
layout.addWidget(self.btn_action)
@@ -21,11 +21,17 @@ from qtpy.QtWidgets import (
QWidget,
)
LABEL_WIDTH = 118
ROW_MARGINS = (4, 0, 4, 0)
ROW_SPACING = 6
class Group(QGroupBox):
def __init__(self, label, widgets):
super().__init__(label)
self.layout = QVBoxLayout(self) # type: ignore
self.layout.setContentsMargins(6, 6, 6, 6)
self.layout.setSpacing(4)
for widget in widgets:
self.layout.addWidget(widget) # type: ignore
@@ -34,16 +40,14 @@ class NumberIndicator(QWidget):
def __init__(self, label="", unit=None, highlight=False, decimals=3):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
layout.setContentsMargins(*ROW_MARGINS)
layout.setSpacing(ROW_SPACING)
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setFixedWidth(LABEL_WIDTH)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.val = QLabel("-")
self.val.setAlignment(Qt.AlignTop) # type: ignore
# self.val.setFixedWidth(140)
layout.addWidget(self.val)
self.unit = unit
self.highlight = highlight
@@ -85,12 +89,11 @@ class InputNumberField(QWidget):
):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
layout.setContentsMargins(*ROW_MARGINS)
layout.setSpacing(ROW_SPACING)
self.identifier = identifier
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setFixedWidth(LABEL_WIDTH)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.val = QDoubleSpinBox()
@@ -102,7 +105,6 @@ class InputNumberField(QWidget):
self.val.setSuffix(" " + unit)
if prefix is not None:
self.val.setPrefix(prefix + " ")
# self.val.setFixedWidth(140)
layout.addWidget(self.val)
def set_number(self, number):
@@ -124,19 +126,18 @@ class InputNumberField(QWidget):
class ComboBox(QWidget):
def __init__(self, identifier="", label="", enums=[]):
def __init__(self, identifier="", label="", enums=None):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
layout.setContentsMargins(*ROW_MARGINS)
layout.setSpacing(ROW_SPACING)
self.identifier = identifier
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setFixedWidth(LABEL_WIDTH)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.value = QComboBox()
for entry in enums:
for entry in enums or []:
self.value.addItem(entry)
layout.addWidget(self.value)
@@ -168,15 +169,15 @@ class Button(QWidget):
def __init__(self, label=None, label_button: str = "", enabled=False):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
layout.setContentsMargins(*ROW_MARGINS)
layout.setSpacing(ROW_SPACING)
if label is not None:
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setFixedWidth(LABEL_WIDTH)
layout.addWidget(self.label)
self.button = QPushButton(label_button)
if label is not None:
self.button.setFixedWidth(160)
self.button.setFixedWidth(130)
self.enable_button(enabled)
layout.addWidget(self.button)
@@ -204,11 +205,10 @@ class TextIndicator(QWidget):
def __init__(self, label):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
layout.setContentsMargins(*ROW_MARGINS)
layout.setSpacing(ROW_SPACING)
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setFixedWidth(LABEL_WIDTH)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.text = QLabel("-")
@@ -223,84 +223,3 @@ class TextIndicator(QWidget):
def setColor(self, color: str):
self.text.setStyleSheet(f"QLabel {{color:{color}}}")
# class Button(QWidget):
# def __init__(self, label, label_button):
# super().__init__()
# layout = QHBoxLayout(self)
# layout.setContentsMargins(10, 0, 0, 0)
# layout.setSpacing(0)
# self.label = QLabel(label)
# self.label.setFixedWidth(150)
# layout.addWidget(self.label)
# self.button = QPushButton(label_button)
# self.button.setStyleSheet("color: black; background-color: dodgerblue;")
# self.button.setFixedWidth(160)
# layout.addWidget(self.button)
# def set_on_press(self, func):
# """Connect a function to the button press."""
# self.button.clicked.connect(func)
# def enable_button(self):
# self.button.setEnabled(True)
# self.button.setStyleSheet("color: black; background-color: dodgerblue;")
# def disable_button(self):
# self.button.setEnabled(False)
# self.button.setStyleSheet("color: black; background-color: grey;")
# def set_button_text(self, text):
# self.button.setText(text)
# class LED(QWidget):
# def __init__(self, states, colors, label):
# super().__init__()
# self.states = states
# self.colors = colors
# layout = QHBoxLayout(self)
# layout.setContentsMargins(10, 0, 0, 0)
# layout.setSpacing(0)
# self.label = QLabel(label)
# self.label.setFixedWidth(150)
# layout.addWidget(self.label)
# self.led = QLabel()
# self.led.setFixedWidth(160)
# layout.addWidget(self.led)
# def apply_color(self, val):
# color = self.colors[self.states.index(val)]
# self.led.setStyleSheet(f"background-color: {color}; border: 1px solid black;")
# class InputTextField(QWidget):
# def __init__(self, topic, label):
# super().__init__()
# self.topic = topic
# layout = QHBoxLayout(self)
# layout.setContentsMargins(10, 0, 0, 0)
# layout.setSpacing(0)
# self.label = QLabel(label)
# self.label.setFixedWidth(140)
# self.label.setContentsMargins(0, 0, 10, 0)
# self.label.setWordWrap(True)
# layout.addWidget(self.label)
# self.val = QLineEdit()
# self.val.setPlaceholderText('0')
# # self.val.setFixedWidth(140)
# layout.addWidget(self.val)
# def set_text(self, text):
# self.val.setText(text)
# def has_focus(self) -> bool:
# return self.val.hasFocus()
# def text(self) -> str:
# return self.val.text()
# def set_on_return(self, func):
# """Connect a function to the Enter/Return key press."""
# self.val.returnPressed.connect(
# partial(func, self.val, self.topic, lambda: self.val.text())
# )