mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-11 17:15:43 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d363e58d6 | |||
| a9b808d86a | |||
| 7018506872 | |||
| b5e2c83492 | |||
| 08a39bf41c | |||
| b9f2abcb21 | |||
| 88996e8cde | |||
| b1d5fbbfad | |||
| be0915c054 | |||
| 767a492613 |
@@ -144,6 +144,9 @@ tests:
|
|||||||
coverage_report:
|
coverage_report:
|
||||||
coverage_format: cobertura
|
coverage_format: cobertura
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
|
paths:
|
||||||
|
- tests/reference_failures/
|
||||||
|
when: always
|
||||||
|
|
||||||
test-matrix:
|
test-matrix:
|
||||||
parallel:
|
parallel:
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ class BECConnector(BECWidget):
|
|||||||
print("No more connections. Shutting down GUI BEC client.")
|
print("No more connections. Shutting down GUI BEC client.")
|
||||||
self.bec_dispatcher.disconnect_all()
|
self.bec_dispatcher.disconnect_all()
|
||||||
self.client.shutdown()
|
self.client.shutdown()
|
||||||
|
if hasattr(super(), "cleanup"):
|
||||||
|
super().cleanup()
|
||||||
|
|
||||||
# def closeEvent(self, event):
|
# def closeEvent(self, event):
|
||||||
# self.cleanup()
|
# self.cleanup()
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
class BECWidget:
|
class BECWidget:
|
||||||
"""Base class for all BEC widgets."""
|
"""Base class for all BEC widgets."""
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if hasattr(self, "cleanup"):
|
||||||
|
self.cleanup()
|
||||||
|
if hasattr(super(), "closeEvent"):
|
||||||
|
super().closeEvent(event)
|
||||||
|
|||||||
@@ -58,11 +58,12 @@ class DesignerPluginGenerator:
|
|||||||
os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
|
os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self, validate=True):
|
||||||
if self._excluded:
|
if self._excluded:
|
||||||
print(f"Plugin {self.widget.__name__} is excluded from generation.")
|
print(f"Plugin {self.widget.__name__} is excluded from generation.")
|
||||||
return
|
return
|
||||||
self._check_class_validity()
|
if validate:
|
||||||
|
self._check_class_validity()
|
||||||
self._load_templates()
|
self._load_templates()
|
||||||
self._write_templates()
|
self._write_templates()
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ class DesignerPluginGenerator:
|
|||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
# from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
|
# from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
|
||||||
from bec_widgets.widgets.dock import BECDockArea
|
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
|
||||||
|
|
||||||
generator = DesignerPluginGenerator(BECDockArea)
|
generator = DesignerPluginGenerator(SpinnerWidget)
|
||||||
generator.run()
|
generator.run(validate=False)
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image, ImageChops
|
||||||
|
from qtpy.QtGui import QPixmap
|
||||||
|
|
||||||
|
import bec_widgets
|
||||||
|
|
||||||
|
REFERENCE_DIR = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/references"
|
||||||
|
)
|
||||||
|
REFERENCE_DIR_FAILURES = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/reference_failures"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_images(image1_path: str, reference_image_path: str):
|
||||||
|
"""
|
||||||
|
Load two images and compare them pixel by pixel
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image1_path(str): The path to the first image
|
||||||
|
reference_image_path(str): The path to the reference image
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the images are different
|
||||||
|
"""
|
||||||
|
image1 = Image.open(image1_path)
|
||||||
|
image2 = Image.open(reference_image_path)
|
||||||
|
if image1.size != image2.size:
|
||||||
|
raise ValueError("Image size has changed")
|
||||||
|
diff = ImageChops.difference(image1, image2)
|
||||||
|
if diff.getbbox():
|
||||||
|
# copy image1 to the reference directory to upload as artifact
|
||||||
|
os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
|
||||||
|
image_name = os.path.join(REFERENCE_DIR_FAILURES, os.path.basename(image1_path))
|
||||||
|
image1.save(image_name)
|
||||||
|
print(f"Image saved to {image_name}")
|
||||||
|
|
||||||
|
raise ValueError("Images are different")
|
||||||
|
|
||||||
|
|
||||||
|
def snap_and_compare(widget: any, output_directory: str, suffix: str = ""):
|
||||||
|
"""
|
||||||
|
Save a rendering of a widget and compare it to a reference image
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget(any): The widget to render
|
||||||
|
output_directory(str): The directory to save the image to
|
||||||
|
suffix(str): A suffix to append to the image name
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the images are different
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
snap_and_compare(widget, tmpdir, suffix="started")
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(output_directory, str):
|
||||||
|
output_directory = str(output_directory)
|
||||||
|
|
||||||
|
os_suffix = sys.platform
|
||||||
|
|
||||||
|
name = (
|
||||||
|
f"{widget.__class__.__name__}_{suffix}_{os_suffix}.png"
|
||||||
|
if suffix
|
||||||
|
else f"{widget.__class__.__name__}_{os_suffix}.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save the widget to a pixmap
|
||||||
|
test_image_path = os.path.join(output_directory, name)
|
||||||
|
pixmap = QPixmap(widget.size())
|
||||||
|
widget.render(pixmap)
|
||||||
|
pixmap.save(test_image_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
reference_path = os.path.join(REFERENCE_DIR, f"{widget.__class__.__name__}")
|
||||||
|
reference_image_path = os.path.join(reference_path, name)
|
||||||
|
|
||||||
|
if not os.path.exists(reference_image_path):
|
||||||
|
raise ValueError(f"Reference image not found: {reference_image_path}")
|
||||||
|
|
||||||
|
compare_images(test_image_path, reference_image_path)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
image = Image.open(test_image_path)
|
||||||
|
os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
|
||||||
|
image_name = os.path.join(REFERENCE_DIR_FAILURES, name)
|
||||||
|
image.save(image_name)
|
||||||
|
print(f"Image saved to {image_name}")
|
||||||
|
raise
|
||||||
@@ -27,7 +27,7 @@ class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return ""
|
return "BEC Core Widgets"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return QIcon()
|
return QIcon()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class BECStatusBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return ""
|
return "BEC Core Widgets"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return QIcon()
|
return QIcon()
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
|||||||
Start ``Backend`` process and render Pyte output as text.
|
Start ``Backend`` process and render Pyte output as text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent, numColumns, numLines, **kwargs):
|
def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
# file descriptor to communicate with the subprocess
|
# file descriptor to communicate with the subprocess
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
from bec_lib.messages import ScanQueueMessage
|
||||||
|
from qtpy.QtCore import Property, Signal, Slot
|
||||||
|
from qtpy.QtGui import QDoubleValidator
|
||||||
|
from qtpy.QtWidgets import QDoubleSpinBox, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils import UILoader
|
||||||
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBox(BECConnector, QWidget):
|
||||||
|
"""A widget that controls a single device."""
|
||||||
|
|
||||||
|
ui_file = "device_box.ui"
|
||||||
|
dimensions = (234, 224)
|
||||||
|
device_changed = Signal(str, str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, device=None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
QWidget.__init__(self, parent=parent)
|
||||||
|
self.get_bec_shortcuts()
|
||||||
|
self._device = ""
|
||||||
|
self._limits = None
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
if device is not None:
|
||||||
|
self.device = device
|
||||||
|
self.init_device()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
self.device_changed.connect(self.on_device_change)
|
||||||
|
|
||||||
|
current_path = os.path.dirname(__file__)
|
||||||
|
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
self.layout.addWidget(self.ui)
|
||||||
|
self.layout.setSpacing(0)
|
||||||
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# fix the size of the device box
|
||||||
|
db = self.ui.device_box
|
||||||
|
db.setFixedHeight(self.dimensions[0])
|
||||||
|
db.setFixedWidth(self.dimensions[1])
|
||||||
|
|
||||||
|
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
|
||||||
|
self.ui.stop.clicked.connect(self.on_stop)
|
||||||
|
self.ui.tweak_right.clicked.connect(self.on_tweak_right)
|
||||||
|
self.ui.tweak_right.setToolTip("Tweak right")
|
||||||
|
self.ui.tweak_left.clicked.connect(self.on_tweak_left)
|
||||||
|
self.ui.tweak_left.setToolTip("Tweak left")
|
||||||
|
self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
|
||||||
|
|
||||||
|
self.setpoint_validator = QDoubleValidator()
|
||||||
|
self.ui.setpoint.setValidator(self.setpoint_validator)
|
||||||
|
self.ui.spinner_widget.start()
|
||||||
|
|
||||||
|
def init_device(self):
|
||||||
|
if self.device in self.dev:
|
||||||
|
data = self.dev[self.device].read()
|
||||||
|
self.on_device_readback({"signals": data}, {})
|
||||||
|
|
||||||
|
@Property(str)
|
||||||
|
def device(self):
|
||||||
|
return self._device
|
||||||
|
|
||||||
|
@device.setter
|
||||||
|
def device(self, value):
|
||||||
|
if not value or not isinstance(value, str):
|
||||||
|
return
|
||||||
|
old_device = self._device
|
||||||
|
self._device = value
|
||||||
|
self.device_changed.emit(old_device, value)
|
||||||
|
|
||||||
|
@Slot(str, str)
|
||||||
|
def on_device_change(self, old_device: str, new_device: str):
|
||||||
|
if new_device not in self.dev:
|
||||||
|
print(f"Device {new_device} not found in the device list")
|
||||||
|
return
|
||||||
|
print(f"Device changed from {old_device} to {new_device}")
|
||||||
|
self.init_device()
|
||||||
|
self.bec_dispatcher.disconnect_slot(
|
||||||
|
self.on_device_readback, MessageEndpoints.device_readback(old_device)
|
||||||
|
)
|
||||||
|
self.bec_dispatcher.connect_slot(
|
||||||
|
self.on_device_readback, MessageEndpoints.device_readback(new_device)
|
||||||
|
)
|
||||||
|
self.ui.device_box.setTitle(new_device)
|
||||||
|
self.ui.readback.setToolTip(f"{self.device} readback")
|
||||||
|
self.ui.setpoint.setToolTip(f"{self.device} setpoint")
|
||||||
|
self.ui.step_size.setToolTip(f"Step size for {new_device}")
|
||||||
|
|
||||||
|
precision = self.dev[new_device].precision
|
||||||
|
if precision is not None:
|
||||||
|
self.ui.step_size.setDecimals(precision)
|
||||||
|
self.ui.step_size.setValue(10**-precision * 10)
|
||||||
|
|
||||||
|
@Slot(dict, dict)
|
||||||
|
def on_device_readback(self, msg_content: dict, metadata: dict):
|
||||||
|
signals = msg_content.get("signals", {})
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
hinted_signals = self.dev[self.device]._hints
|
||||||
|
precision = self.dev[self.device].precision
|
||||||
|
|
||||||
|
readback_val = None
|
||||||
|
setpoint_val = None
|
||||||
|
|
||||||
|
if len(hinted_signals) == 1:
|
||||||
|
signal = hinted_signals[0]
|
||||||
|
readback_val = signals.get(signal, {}).get("value")
|
||||||
|
|
||||||
|
if f"{self.device}_setpoint" in signals:
|
||||||
|
setpoint_val = signals.get(f"{self.device}_setpoint", {}).get("value")
|
||||||
|
|
||||||
|
if f"{self.device}_motor_is_moving" in signals:
|
||||||
|
is_moving = signals.get(f"{self.device}_motor_is_moving", {}).get("value")
|
||||||
|
if is_moving:
|
||||||
|
self.ui.spinner_widget.start()
|
||||||
|
self.ui.spinner_widget.setToolTip("Device is moving")
|
||||||
|
else:
|
||||||
|
self.ui.spinner_widget.stop()
|
||||||
|
self.ui.spinner_widget.setToolTip("Device is idle")
|
||||||
|
|
||||||
|
if readback_val is not None:
|
||||||
|
self.ui.readback.setText(f"{readback_val:.{precision}f}")
|
||||||
|
|
||||||
|
if setpoint_val is not None:
|
||||||
|
self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")
|
||||||
|
|
||||||
|
limits = self.dev[self.device].limits
|
||||||
|
self.update_limits(limits)
|
||||||
|
if limits is not None and readback_val is not None and limits[0] != limits[1]:
|
||||||
|
pos = (readback_val - limits[0]) / (limits[1] - limits[0])
|
||||||
|
self.ui.position_indicator.on_position_update(pos)
|
||||||
|
|
||||||
|
def update_limits(self, limits):
|
||||||
|
if limits == self._limits:
|
||||||
|
return
|
||||||
|
self._limits = limits
|
||||||
|
if limits is not None and limits[0] != limits[1]:
|
||||||
|
self.ui.position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
|
||||||
|
self.setpoint_validator.setRange(limits[0], limits[1])
|
||||||
|
else:
|
||||||
|
self.ui.position_indicator.setToolTip("No limits set")
|
||||||
|
self.setpoint_validator.setRange(float("-inf"), float("inf"))
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def on_stop(self):
|
||||||
|
request_id = str(uuid.uuid4())
|
||||||
|
params = {
|
||||||
|
"device": self.device,
|
||||||
|
"rpc_id": request_id,
|
||||||
|
"func": "stop",
|
||||||
|
"args": [],
|
||||||
|
"kwargs": {},
|
||||||
|
}
|
||||||
|
msg = ScanQueueMessage(
|
||||||
|
scan_type="device_rpc",
|
||||||
|
parameter=params,
|
||||||
|
queue="emergency",
|
||||||
|
metadata={"RID": request_id, "response": False},
|
||||||
|
)
|
||||||
|
self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def step_size(self):
|
||||||
|
return self.ui.step_size.value()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def on_tweak_right(self):
|
||||||
|
self.dev[self.device].move(self.step_size, relative=True)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def on_tweak_left(self):
|
||||||
|
self.dev[self.device].move(-self.step_size, relative=True)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def on_setpoint_change(self):
|
||||||
|
self.ui.setpoint.clearFocus()
|
||||||
|
setpoint = self.ui.setpoint.text()
|
||||||
|
self.dev[self.device].move(float(setpoint), relative=False)
|
||||||
|
self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
|
||||||
|
self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import qdarktheme
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
qdarktheme.setup_theme("light")
|
||||||
|
widget = DeviceBox(device="samx")
|
||||||
|
|
||||||
|
widget.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{'files': ['device_box.py']}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>251</width>
|
||||||
|
<height>289</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>192</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>16777215</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="device_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>Device Name</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0">
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QDoubleSpinBox" name="step_size"/>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2">
|
||||||
|
<widget class="QToolButton" name="tweak_right">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>50</width>
|
||||||
|
<height>50</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>50</width>
|
||||||
|
<height>50</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="arrowType">
|
||||||
|
<enum>Qt::ArrowType::RightArrow</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="3">
|
||||||
|
<widget class="QLineEdit" name="setpoint"/>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QToolButton" name="tweak_left">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>50</width>
|
||||||
|
<height>50</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>50</width>
|
||||||
|
<height>50</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="arrowType">
|
||||||
|
<enum>Qt::ArrowType::LeftArrow</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0" colspan="3">
|
||||||
|
<widget class="QPushButton" name="stop">
|
||||||
|
<property name="text">
|
||||||
|
<string>Stop</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0" colspan="3">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeType">
|
||||||
|
<enum>QSizePolicy::Policy::Expanding</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="SpinnerWidget" name="spinner_widget">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="PositionIndicator" name="position_indicator"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="readback">
|
||||||
|
<property name="text">
|
||||||
|
<string>Position</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>SpinnerWidget</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>spinner_widget</header>
|
||||||
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>PositionIndicator</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>position_indicator</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtGui import QIcon
|
||||||
|
|
||||||
|
from bec_widgets.widgets.device_box.device_box import DeviceBox
|
||||||
|
|
||||||
|
DOM_XML = """
|
||||||
|
<ui language='c++'>
|
||||||
|
<widget class='DeviceBox' name='device_box'>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._form_editor = None
|
||||||
|
|
||||||
|
def createWidget(self, parent):
|
||||||
|
t = DeviceBox(parent)
|
||||||
|
return t
|
||||||
|
|
||||||
|
def domXml(self):
|
||||||
|
return DOM_XML
|
||||||
|
|
||||||
|
def group(self):
|
||||||
|
return "Device Control"
|
||||||
|
|
||||||
|
def icon(self):
|
||||||
|
return QIcon()
|
||||||
|
|
||||||
|
def includeFile(self):
|
||||||
|
return "device_box"
|
||||||
|
|
||||||
|
def initialize(self, form_editor):
|
||||||
|
self._form_editor = form_editor
|
||||||
|
|
||||||
|
def isContainer(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isInitialized(self):
|
||||||
|
return self._form_editor is not None
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "DeviceBox"
|
||||||
|
|
||||||
|
def toolTip(self):
|
||||||
|
return "A widget for controlling a single positioner. "
|
||||||
|
|
||||||
|
def whatsThis(self):
|
||||||
|
return self.toolTip()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from bec_widgets.widgets.device_box.device_box import DeviceBox
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceControlLine(DeviceBox):
|
||||||
|
"""A widget that controls a single device."""
|
||||||
|
|
||||||
|
ui_file = "device_control_line.ui"
|
||||||
|
dimensions = (70, 800) # height, width
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import qdarktheme
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
qdarktheme.setup_theme("light")
|
||||||
|
widget = DeviceControlLine(device="samy")
|
||||||
|
|
||||||
|
widget.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>785</width>
|
||||||
|
<height>91</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>16777215</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="device_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>Device Name</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="PositionIndicator" name="position_indicator"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="readback">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>150</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>150</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Position</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="setpoint">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>150</width>
|
||||||
|
<height>24</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>150</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="stop">
|
||||||
|
<property name="text">
|
||||||
|
<string>Stop</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="tweak_left">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="arrowType">
|
||||||
|
<enum>Qt::ArrowType::LeftArrow</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDoubleSpinBox" name="step_size"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="tweak_right">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="arrowType">
|
||||||
|
<enum>Qt::ArrowType::RightArrow</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="SpinnerWidget" name="spinner_widget">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>SpinnerWidget</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>spinner_widget</header>
|
||||||
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>PositionIndicator</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>position_indicator</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
def main(): # pragma: no cover
|
||||||
|
from qtpy import PYSIDE6
|
||||||
|
|
||||||
|
if not PYSIDE6:
|
||||||
|
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||||
|
return
|
||||||
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
|
from bec_widgets.widgets.device_box.device_box_plugin import DeviceBoxPlugin
|
||||||
|
|
||||||
|
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceBoxPlugin())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
||||||
@@ -830,6 +830,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
|||||||
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
|
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
|
||||||
)
|
)
|
||||||
|
|
||||||
def cleanup(self):
|
# def cleanup(self):
|
||||||
self.clear_all()
|
# self.clear_all()
|
||||||
super().cleanup()
|
# super().cleanup()
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from qtpy.QtCore import Qt, Slot
|
||||||
|
from qtpy.QtGui import QPainter, QPen
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
|
|
||||||
|
class PositionIndicator(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.position = 0.5
|
||||||
|
self.min_value = 0
|
||||||
|
self.max_value = 100
|
||||||
|
self.scaling_factor = 0.5
|
||||||
|
self.setMinimumHeight(10)
|
||||||
|
|
||||||
|
def set_range(self, min_value, max_value):
|
||||||
|
self.min_value = min_value
|
||||||
|
self.max_value = max_value
|
||||||
|
|
||||||
|
@Slot(float)
|
||||||
|
def on_position_update(self, position: float):
|
||||||
|
self.position = position
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
|
||||||
|
width = self.width()
|
||||||
|
height = self.height()
|
||||||
|
|
||||||
|
# Draw horizontal line
|
||||||
|
painter.setPen(Qt.black)
|
||||||
|
painter.drawLine(0, height // 2, width, height // 2)
|
||||||
|
|
||||||
|
# Draw shorter vertical line at the current position
|
||||||
|
x_pos = int(self.position * width)
|
||||||
|
painter.setPen(QPen(Qt.red, 2))
|
||||||
|
short_line_height = int(height * self.scaling_factor)
|
||||||
|
painter.drawLine(
|
||||||
|
x_pos,
|
||||||
|
(height // 2) - (short_line_height // 2),
|
||||||
|
x_pos,
|
||||||
|
(height // 2) + (short_line_height // 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw thicker vertical lines at the ends
|
||||||
|
end_line_pen = QPen(Qt.blue, 5)
|
||||||
|
painter.setPen(end_line_pen)
|
||||||
|
painter.drawLine(0, 0, 0, height)
|
||||||
|
painter.drawLine(width - 1, 0, width - 1, height)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from qtpy.QtWidgets import QApplication, QSlider, QVBoxLayout
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
|
||||||
|
position_indicator = PositionIndicator()
|
||||||
|
slider = QSlider(Qt.Horizontal)
|
||||||
|
slider.valueChanged.connect(lambda value: position_indicator.on_position_update(value / 100))
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(position_indicator)
|
||||||
|
layout.addWidget(slider)
|
||||||
|
|
||||||
|
widget = QWidget()
|
||||||
|
widget.setLayout(layout)
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
app.exec_()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{'files': ['position_indicator.py']}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtGui import QIcon
|
||||||
|
|
||||||
|
from bec_widgets.widgets.position_indicator.position_indicator import PositionIndicator
|
||||||
|
|
||||||
|
DOM_XML = """
|
||||||
|
<ui language='c++'>
|
||||||
|
<widget class='PositionIndicator' name='position_indicator'>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._form_editor = None
|
||||||
|
|
||||||
|
def createWidget(self, parent):
|
||||||
|
t = PositionIndicator(parent)
|
||||||
|
return t
|
||||||
|
|
||||||
|
def domXml(self):
|
||||||
|
return DOM_XML
|
||||||
|
|
||||||
|
def group(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def icon(self):
|
||||||
|
return QIcon()
|
||||||
|
|
||||||
|
def includeFile(self):
|
||||||
|
return "position_indicator"
|
||||||
|
|
||||||
|
def initialize(self, form_editor):
|
||||||
|
self._form_editor = form_editor
|
||||||
|
|
||||||
|
def isContainer(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isInitialized(self):
|
||||||
|
return self._form_editor is not None
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "PositionIndicator"
|
||||||
|
|
||||||
|
def toolTip(self):
|
||||||
|
return "PositionIndicator"
|
||||||
|
|
||||||
|
def whatsThis(self):
|
||||||
|
return self.toolTip()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
def main(): # pragma: no cover
|
||||||
|
from qtpy import PYSIDE6
|
||||||
|
|
||||||
|
if not PYSIDE6:
|
||||||
|
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||||
|
return
|
||||||
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
|
from bec_widgets.widgets.position_indicator.position_indicator_plugin import (
|
||||||
|
PositionIndicatorPlugin,
|
||||||
|
)
|
||||||
|
|
||||||
|
QPyDesignerCustomWidgetCollection.addCustomWidget(PositionIndicatorPlugin())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
def main(): # pragma: no cover
|
||||||
|
from qtpy import PYSIDE6
|
||||||
|
|
||||||
|
if not PYSIDE6:
|
||||||
|
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||||
|
return
|
||||||
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
|
from bec_widgets.widgets.spinner.spinner_widget_plugin import SpinnerWidgetPlugin
|
||||||
|
|
||||||
|
QPyDesignerCustomWidgetCollection.addCustomWidget(SpinnerWidgetPlugin())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import qdarktheme
|
||||||
|
from qtpy.QtCore import QRect, Qt, QTimer
|
||||||
|
from qtpy.QtGui import QColor, QPainter, QPen
|
||||||
|
from qtpy.QtWidgets import QApplication, QMainWindow, QWidget
|
||||||
|
|
||||||
|
|
||||||
|
def ease_in_out_sine(t):
|
||||||
|
return 1 - np.sin(np.pi * t)
|
||||||
|
|
||||||
|
|
||||||
|
class SpinnerWidget(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.angle = 0
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.timeout.connect(self.rotate)
|
||||||
|
self.time = 0
|
||||||
|
self.duration = 50
|
||||||
|
self.speed = 50
|
||||||
|
self._started = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self._started:
|
||||||
|
return
|
||||||
|
self.timer.start(self.speed)
|
||||||
|
self._started = True
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if not self._started:
|
||||||
|
return
|
||||||
|
self.timer.stop()
|
||||||
|
self._started = False
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def rotate(self):
|
||||||
|
self.time = (self.time + 1) % self.duration
|
||||||
|
t = self.time / self.duration
|
||||||
|
easing_value = ease_in_out_sine(t)
|
||||||
|
self.angle -= (20 * easing_value) % 360 + 10
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
size = min(self.width(), self.height())
|
||||||
|
rect = QRect(0, 0, size, size)
|
||||||
|
|
||||||
|
background_color = QColor(200, 200, 200, 50)
|
||||||
|
line_width = 5
|
||||||
|
|
||||||
|
color_palette = qdarktheme.load_palette()
|
||||||
|
|
||||||
|
color = QColor(color_palette.accent().color())
|
||||||
|
|
||||||
|
rect.adjust(line_width, line_width, -line_width, -line_width)
|
||||||
|
|
||||||
|
# Background arc
|
||||||
|
painter.setPen(QPen(background_color, line_width, Qt.SolidLine))
|
||||||
|
adjusted_rect = QRect(rect.left(), rect.top(), rect.width(), rect.height())
|
||||||
|
painter.drawArc(adjusted_rect, 0, 360 * 16)
|
||||||
|
|
||||||
|
if self._started:
|
||||||
|
# Foreground arc
|
||||||
|
pen = QPen(color, line_width, Qt.SolidLine)
|
||||||
|
pen.setCapStyle(Qt.RoundCap)
|
||||||
|
painter.setPen(pen)
|
||||||
|
proportion = 1 / 4
|
||||||
|
angle_span = int(proportion * 360 * 16)
|
||||||
|
angle_span += angle_span * ease_in_out_sine(self.time / self.duration)
|
||||||
|
painter.drawArc(adjusted_rect, self.angle * 16, int(angle_span))
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = QMainWindow()
|
||||||
|
widget = SpinnerWidget()
|
||||||
|
widget.start()
|
||||||
|
window.setCentralWidget(widget)
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{'files': ['spinner.py']}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtGui import QIcon
|
||||||
|
|
||||||
|
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
|
||||||
|
|
||||||
|
DOM_XML = """
|
||||||
|
<ui language='c++'>
|
||||||
|
<widget class='SpinnerWidget' name='spinner_widget'>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SpinnerWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._form_editor = None
|
||||||
|
|
||||||
|
def createWidget(self, parent):
|
||||||
|
t = SpinnerWidget(parent)
|
||||||
|
return t
|
||||||
|
|
||||||
|
def domXml(self):
|
||||||
|
return DOM_XML
|
||||||
|
|
||||||
|
def group(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def icon(self):
|
||||||
|
return QIcon()
|
||||||
|
|
||||||
|
def includeFile(self):
|
||||||
|
return "spinner_widget"
|
||||||
|
|
||||||
|
def initialize(self, form_editor):
|
||||||
|
self._form_editor = form_editor
|
||||||
|
|
||||||
|
def isContainer(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isInitialized(self):
|
||||||
|
return self._form_editor is not None
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "SpinnerWidget"
|
||||||
|
|
||||||
|
def toolTip(self):
|
||||||
|
return "SpinnerWidget"
|
||||||
|
|
||||||
|
def whatsThis(self):
|
||||||
|
return self.toolTip()
|
||||||
@@ -49,13 +49,6 @@ class VSCodeEditor(WebsiteWidget):
|
|||||||
break
|
break
|
||||||
self.set_url(self._url)
|
self.set_url(self._url)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
"""
|
|
||||||
Hook for the close event to terminate the server.
|
|
||||||
"""
|
|
||||||
self.cleanup_vscode()
|
|
||||||
super().closeEvent(event)
|
|
||||||
|
|
||||||
def cleanup_vscode(self):
|
def cleanup_vscode(self):
|
||||||
"""
|
"""
|
||||||
Cleanup the VSCode editor.
|
Cleanup the VSCode editor.
|
||||||
@@ -72,13 +65,6 @@ class VSCodeEditor(WebsiteWidget):
|
|||||||
self.cleanup_vscode()
|
self.cleanup_vscode()
|
||||||
return super().cleanup()
|
return super().cleanup()
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""
|
|
||||||
Close the widget.
|
|
||||||
"""
|
|
||||||
self.cleanup_vscode()
|
|
||||||
return super().close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
@@ -69,6 +69,6 @@ if __name__ == "__main__":
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
mainWin = WebsiteWidget("https://scilog.psi.ch")
|
mainWin = WebsiteWidget(url="https://scilog.psi.ch")
|
||||||
mainWin.show()
|
mainWin.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -49,11 +49,19 @@ class FakePositioner(FakeDevice):
|
|||||||
self.read_value = read_value
|
self.read_value = read_value
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def precision(self):
|
||||||
|
return 3
|
||||||
|
|
||||||
def set_read_value(self, value):
|
def set_read_value(self, value):
|
||||||
self.read_value = value
|
self.read_value = value
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
return {self.name: {"value": self.read_value}}
|
return {
|
||||||
|
self.name: {"value": self.read_value},
|
||||||
|
f"{self.name}_setpoint": {"value": self.read_value},
|
||||||
|
f"{self.name}_motor_is_moving": {"value": 0},
|
||||||
|
}
|
||||||
|
|
||||||
def set_limits(self, limits):
|
def set_limits(self, limits):
|
||||||
self.limits = limits
|
self.limits = limits
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
from bec_lib.messages import ScanQueueMessage
|
||||||
|
from qtpy.QtGui import QValidator
|
||||||
|
|
||||||
|
from bec_widgets.widgets.device_box.device_box import DeviceBox
|
||||||
|
|
||||||
|
from .client_mocks import mocked_client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def device_box(qtbot, mocked_client):
|
||||||
|
with mock.patch("bec_widgets.widgets.device_box.device_box.uuid.uuid4") as mock_uuid:
|
||||||
|
mock_uuid.return_value = "fake_uuid"
|
||||||
|
db = DeviceBox(device="samx", client=mocked_client)
|
||||||
|
qtbot.addWidget(db)
|
||||||
|
yield db
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_box(device_box):
|
||||||
|
assert device_box.device == "samx"
|
||||||
|
data = device_box.dev["samx"].read()
|
||||||
|
|
||||||
|
setpoint_text = device_box.ui.setpoint.text()
|
||||||
|
# check that the setpoint is taken correctly after init
|
||||||
|
assert float(setpoint_text) == data["samx_setpoint"]["value"]
|
||||||
|
|
||||||
|
# check that the precision is taken correctly after init
|
||||||
|
precision = device_box.dev["samx"].precision
|
||||||
|
assert setpoint_text == f"{data['samx_setpoint']['value']:.{precision}f}"
|
||||||
|
|
||||||
|
# check that the step size is set according to the device precision
|
||||||
|
assert device_box.ui.step_size.value() == 10**-precision * 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_box_update_limits(device_box):
|
||||||
|
device_box._limits = None
|
||||||
|
device_box.update_limits([0, 10])
|
||||||
|
assert device_box._limits == [0, 10]
|
||||||
|
assert device_box.setpoint_validator.bottom() == 0
|
||||||
|
assert device_box.setpoint_validator.top() == 10
|
||||||
|
assert device_box.setpoint_validator.validate("100", 0) == (
|
||||||
|
QValidator.State.Intermediate,
|
||||||
|
"100",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
device_box.update_limits(None)
|
||||||
|
assert device_box._limits is None
|
||||||
|
assert device_box.setpoint_validator.validate("100", 0) == (
|
||||||
|
QValidator.State.Acceptable,
|
||||||
|
"100",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_box_on_stop(device_box):
|
||||||
|
with mock.patch.object(device_box.client.connector, "send") as mock_send:
|
||||||
|
device_box.on_stop()
|
||||||
|
params = {"device": "samx", "rpc_id": "fake_uuid", "func": "stop", "args": [], "kwargs": {}}
|
||||||
|
msg = ScanQueueMessage(
|
||||||
|
scan_type="device_rpc",
|
||||||
|
parameter=params,
|
||||||
|
queue="emergency",
|
||||||
|
metadata={"RID": "fake_uuid", "response": False},
|
||||||
|
)
|
||||||
|
mock_send.assert_called_once_with(MessageEndpoints.scan_queue_request(), msg)
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_box_setpoint_change(device_box):
|
||||||
|
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
|
||||||
|
device_box.ui.setpoint.setText("100")
|
||||||
|
device_box.on_setpoint_change()
|
||||||
|
mock_move.assert_called_once_with(100, relative=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_box_on_tweak_right(device_box):
|
||||||
|
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
|
||||||
|
device_box.ui.step_size.setValue(0.1)
|
||||||
|
device_box.on_tweak_right()
|
||||||
|
mock_move.assert_called_once_with(0.1, relative=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_box_on_tweak_left(device_box):
|
||||||
|
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
|
||||||
|
device_box.ui.step_size.setValue(0.1)
|
||||||
|
device_box.on_tweak_left()
|
||||||
|
mock_move.assert_called_once_with(-0.1, relative=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_box_setpoint_out_of_range(device_box):
|
||||||
|
device_box.update_limits([0, 10])
|
||||||
|
device_box.ui.setpoint.setText("100")
|
||||||
|
device_box.on_setpoint_change()
|
||||||
|
assert device_box.ui.setpoint.text() == "100"
|
||||||
|
assert device_box.ui.setpoint.hasAcceptableInput() == False
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import pytest
|
||||||
|
import qdarktheme
|
||||||
|
|
||||||
|
from bec_widgets.utils.reference_utils import snap_and_compare
|
||||||
|
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def spinner_widget(qtbot):
|
||||||
|
qdarktheme.setup_theme("light")
|
||||||
|
spinner = SpinnerWidget()
|
||||||
|
qtbot.addWidget(spinner)
|
||||||
|
qtbot.waitExposed(spinner)
|
||||||
|
yield spinner
|
||||||
|
|
||||||
|
|
||||||
|
def test_spinner_widget_paint_event(spinner_widget, qtbot):
|
||||||
|
spinner_widget.paintEvent(None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_spinner_widget_rendered(spinner_widget, qtbot, tmpdir):
|
||||||
|
spinner_widget.update()
|
||||||
|
qtbot.wait(200)
|
||||||
|
snap_and_compare(spinner_widget, str(tmpdir), suffix="")
|
||||||
|
|
||||||
|
spinner_widget._started = True
|
||||||
|
spinner_widget.update()
|
||||||
|
qtbot.wait(200)
|
||||||
|
|
||||||
|
snap_and_compare(spinner_widget, str(tmpdir), suffix="started")
|
||||||
@@ -25,54 +25,49 @@ def test_vscode_widget(qtbot, vscode_widget):
|
|||||||
def test_start_server(qtbot, mocked_client):
|
def test_start_server(qtbot, mocked_client):
|
||||||
|
|
||||||
with mock.patch("bec_widgets.widgets.vscode.vscode.subprocess.Popen") as mock_popen:
|
with mock.patch("bec_widgets.widgets.vscode.vscode.subprocess.Popen") as mock_popen:
|
||||||
mock_process = mock.Mock()
|
with mock.patch("bec_widgets.widgets.vscode.vscode.select.select") as mock_select:
|
||||||
mock_process.stdout.fileno.return_value = 1
|
mock_process = mock.Mock()
|
||||||
mock_process.poll.return_value = None
|
mock_process.stdout.fileno.return_value = 1
|
||||||
mock_process.stdout.read.return_value = (
|
mock_process.poll.return_value = None
|
||||||
f"available at http://{VSCodeEditor.host}:{VSCodeEditor.port}?tkn={VSCodeEditor.token}"
|
mock_process.stdout.read.return_value = f"available at http://{VSCodeEditor.host}:{VSCodeEditor.port}?tkn={VSCodeEditor.token}"
|
||||||
)
|
mock_popen.return_value = mock_process
|
||||||
mock_popen.return_value = mock_process
|
mock_select.return_value = [[mock_process.stdout], [], []]
|
||||||
|
|
||||||
widget = VSCodeEditor(client=mocked_client)
|
widget = VSCodeEditor(client=mocked_client)
|
||||||
|
|
||||||
mock_popen.assert_called_once_with(
|
mock_popen.assert_called_once_with(
|
||||||
shlex.split(
|
shlex.split(
|
||||||
f"code serve-web --port {widget.port} --connection-token={widget.token} --accept-server-license-terms"
|
f"code serve-web --port {widget.port} --connection-token={widget.token} --accept-server-license-terms"
|
||||||
),
|
),
|
||||||
text=True,
|
text=True,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
preexec_fn=os.setsid,
|
preexec_fn=os.setsid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def patched_vscode_process(qtbot, vscode_widget):
|
def patched_vscode_process(qtbot, vscode_widget):
|
||||||
with mock.patch("bec_widgets.widgets.vscode.vscode.os.killpg") as mock_killpg:
|
with mock.patch("bec_widgets.widgets.vscode.vscode.os.killpg") as mock_killpg:
|
||||||
with mock.patch("bec_widgets.widgets.vscode.vscode.os.getpgid") as mock_getpgid:
|
with mock.patch("bec_widgets.widgets.vscode.vscode.os.getpgid") as mock_getpgid:
|
||||||
with mock.patch(
|
mock_getpgid.return_value = 123
|
||||||
"bec_widgets.widgets.website.website.WebsiteWidget.closeEvent"
|
vscode_widget.process = mock.Mock()
|
||||||
) as mock_close_event:
|
yield vscode_widget, mock_killpg
|
||||||
mock_getpgid.return_value = 123
|
|
||||||
vscode_widget.process = mock.Mock()
|
|
||||||
yield vscode_widget, mock_killpg, mock_close_event
|
|
||||||
|
|
||||||
|
|
||||||
def test_close_event(qtbot, patched_vscode_process):
|
def test_vscode_cleanup(qtbot, patched_vscode_process):
|
||||||
vscode_patched, mock_killpg, mock_close_event = patched_vscode_process
|
vscode_patched, mock_killpg = patched_vscode_process
|
||||||
vscode_patched.process.pid = 123
|
vscode_patched.process.pid = 123
|
||||||
vscode_patched.process.poll.return_value = None
|
vscode_patched.process.poll.return_value = None
|
||||||
vscode_patched.closeEvent(None)
|
vscode_patched.cleanup()
|
||||||
mock_killpg.assert_called_once_with(123, 15)
|
mock_killpg.assert_called_once_with(123, 15)
|
||||||
vscode_patched.process.wait.assert_called_once()
|
vscode_patched.process.wait.assert_called_once()
|
||||||
mock_close_event.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
def test_close_event_on_terminated_code(qtbot, patched_vscode_process):
|
def test_close_event_on_terminated_code(qtbot, patched_vscode_process):
|
||||||
vscode_patched, mock_killpg, mock_close_event = patched_vscode_process
|
vscode_patched, mock_killpg = patched_vscode_process
|
||||||
vscode_patched.process.pid = 123
|
vscode_patched.process.pid = 123
|
||||||
vscode_patched.process.poll.return_value = 0
|
vscode_patched.process.poll.return_value = 0
|
||||||
vscode_patched.closeEvent(None)
|
vscode_patched.cleanup()
|
||||||
mock_killpg.assert_not_called()
|
mock_killpg.assert_not_called()
|
||||||
vscode_patched.process.wait.assert_not_called()
|
vscode_patched.process.wait.assert_not_called()
|
||||||
mock_close_event.assert_called_once()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user