1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-12 19:50:54 +02:00

Compare commits

...

12 Commits

42 changed files with 2284 additions and 488 deletions

View File

@@ -1,7 +1,7 @@
# This file is a template, and might need editing before it works on your project.
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/python/tags/
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
#commands to run in the Docker container before starting each job.
variables:
DOCKER_TLS_CERTDIR: ""
@@ -23,7 +23,6 @@ workflow:
include:
- template: Security/Secret-Detection.gitlab-ci.yml
# different stages in the pipeline
stages:
- Formatter
@@ -65,7 +64,7 @@ pylint:
- ./pylint/
expire_in: 1 week
rules:
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
pylint-check:
stage: Formatter
@@ -98,7 +97,7 @@ pylint-check:
- ./pylint/
expire_in: 1 week
rules:
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
tests:
stage: test
@@ -112,7 +111,7 @@ tests:
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt6]
- pip install -e .[dev,pyside6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
@@ -124,17 +123,140 @@ tests:
coverage_format: cobertura
path: coverage.xml
tests-3.11:
tests-3.10-pyside6:
extends: "tests"
stage: AdditionalTests
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyside6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.12:
tests-3.12-pyside6:
extends: "tests"
stage: AdditionalTests
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyside6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.10-pyqt5:
extends: "tests"
stage: AdditionalTests
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt5]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.11-pyqt5:
extends: "tests"
stage: AdditionalTests
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt5]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.12-pyqt5:
extends: "tests"
stage: AdditionalTests
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt5]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.10-pyqt6:
extends: "tests"
stage: AdditionalTests
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.11-pyqt6:
extends: "tests"
stage: AdditionalTests
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.12-pyqt6:
extends: "tests"
stage: AdditionalTests
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
end-2-end-conda:
@@ -165,7 +287,7 @@ end-2-end-conda:
- pip install -e ./bec_lib[dev]
- pip install -e ./bec_ipython_client[dev]
- cd ../
- pip install -e .[dev,pyqt6]
- pip install -e .[dev,pyside6]
- cd ./tests/end-2-end
- pytest --start-servers --flush-redis --random-order
@@ -183,7 +305,6 @@ end-2-end-conda:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "production"'
semver:
stage: Deploy
needs: ["tests"]

View File

@@ -2,6 +2,13 @@
## v0.55.0 (2024-05-24)
### Feature
* feat(widgets/progressbar): SpiralProgressBar added with rpc interface ([`76bd0d3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/76bd0d339ac9ae9e8a3baa0d0d4e951ec1d09670))
## v0.54.0 (2024-05-24)
### Build
@@ -162,11 +169,3 @@
## v0.49.1 (2024-04-26)
### Build
* build(pyqt6): fixing PyQt6-Qt6 package to 6.6.3 ([`a222298`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a22229849cbb57c15e4c1bae02d7e52e672f8c4c))
### Fix
* fix(widgets/editor): qscintilla editor removed ([`ab85374`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ab8537483da6c87cb9a0b0f01706208c964f292d))

View File

@@ -1630,3 +1630,255 @@ class BECDockArea(RPCBase, BECGuiClientMixin):
"""
Get all registered RPC objects.
"""
class SpiralProgressBar(RPCBase):
@rpc_call
def get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
"""
@property
@rpc_call
def config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@property
@rpc_call
def rings(self):
"""
None
"""
@rpc_call
def update_config(self, config: "SpiralProgressBarConfig | dict"):
"""
Update the configuration of the widget.
Args:
config(SpiralProgressBarConfig|dict): Configuration to update.
"""
@rpc_call
def add_ring(self, **kwargs) -> "Ring":
"""
Add a new progress bar.
Args:
**kwargs: Keyword arguments for the new progress bar.
Returns:
Ring: Ring object.
"""
@rpc_call
def remove_ring(self, index: "int"):
"""
Remove a progress bar by index.
Args:
index(int): Index of the progress bar to remove.
"""
@rpc_call
def set_precision(self, precision: "int", bar_index: "int" = None):
"""
Set the precision for the progress bars. If bar_index is not provide, the precision will be set for all progress bars.
Args:
precision(int): Precision for the progress bars.
bar_index(int): Index of the progress bar to set the precision for. If provided, only a single precision can be set.
"""
@rpc_call
def set_min_max_values(
self,
min_values: "int | float | list[int | float]",
max_values: "int | float | list[int | float]",
):
"""
Set the minimum and maximum values for the progress bars.
Args:
min_values(int|float | list[float]): Minimum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of minimum values for each progress bar.
max_values(int|float | list[float]): Maximum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of maximum values for each progress bar.
"""
@rpc_call
def set_number_of_bars(self, num_bars: "int"):
"""
Set the number of progress bars to display.
Args:
num_bars(int): Number of progress bars to display.
"""
@rpc_call
def set_value(self, values: "int | list", ring_index: "int" = None):
"""
Set the values for the progress bars.
Args:
values(int | tuple): Value(s) for the progress bars. If multiple progress bars are displayed, provide a tuple of values for each progress bar.
ring_index(int): Index of the progress bar to set the value for. If provided, only a single value can be set.
Examples:
>>> SpiralProgressBar.set_value(50)
>>> SpiralProgressBar.set_value([30, 40, 50]) # (outer, middle, inner)
>>> SpiralProgressBar.set_value(60, bar_index=1) # Set the value for the middle progress bar.
"""
@rpc_call
def set_colors_from_map(self, colormap, color_format: "Literal['RGB', 'HEX']" = "RGB"):
"""
Set the colors for the progress bars from a colormap.
Args:
colormap(str): Name of the colormap.
color_format(Literal["RGB","HEX"]): Format of the returned colors ('RGB', 'HEX').
"""
@rpc_call
def set_colors_directly(
self, colors: "list[str | tuple] | str | tuple", bar_index: "int" = None
):
"""
Set the colors for the progress bars directly.
Args:
colors(list[str | tuple] | str | tuple): Color(s) for the progress bars. If multiple progress bars are displayed, provide a list of colors for each progress bar.
bar_index(int): Index of the progress bar to set the color for. If provided, only a single color can be set.
"""
@rpc_call
def set_line_widths(self, widths: "int | list[int]", bar_index: "int" = None):
"""
Set the line widths for the progress bars.
Args:
widths(int | list[int]): Line width(s) for the progress bars. If multiple progress bars are displayed, provide a list of line widths for each progress bar.
bar_index(int): Index of the progress bar to set the line width for. If provided, only a single line width can be set.
"""
@rpc_call
def set_gap(self, gap: "int"):
"""
Set the gap between the progress bars.
Args:
gap(int): Gap between the progress bars.
"""
@rpc_call
def set_diameter(self, diameter: "int"):
"""
Set the diameter of the widget.
Args:
diameter(int): Diameter of the widget.
"""
@rpc_call
def reset_diameter(self):
"""
Reset the fixed size of the widget.
"""
@rpc_call
def enable_auto_updates(self, enable: "bool" = True):
"""
Enable or disable updates based on scan status. Overrides manual updates.
The behaviour of the whole progress bar widget will be driven by the scan queue status.
Args:
enable(bool): True or False.
Returns:
bool: True if scan segment updates are enabled.
"""
class Ring(RPCBase):
@rpc_call
def get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
"""
@property
@rpc_call
def config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def set_value(self, value: "int | float"):
"""
None
"""
@rpc_call
def set_color(self, color: "str | tuple"):
"""
None
"""
@rpc_call
def set_background(self, color: "str | tuple"):
"""
None
"""
@rpc_call
def set_line_width(self, width: "int"):
"""
None
"""
@rpc_call
def set_min_max_values(self, min_value: "int", max_value: "int"):
"""
None
"""
@rpc_call
def set_start_angle(self, start_angle: "int"):
"""
None
"""
@rpc_call
def set_connections(self, slot: "str", endpoint: "str | EndpointInfo"):
"""
None
"""
@rpc_call
def reset_connection(self):
"""
None
"""

View File

@@ -109,13 +109,14 @@ if __name__ == "__main__": # pragma: no cover
import os
from bec_widgets.utils import BECConnector
from bec_widgets.widgets import BECDock, BECDockArea, BECFigure
from bec_widgets.widgets import BECDock, BECDockArea, BECFigure, SpiralProgressBar
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem
from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase
from bec_widgets.widgets.figure.plots.waveform.waveform import BECWaveform
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import BECCurve
from bec_widgets.widgets.spiral_progress_bar.ring import Ring
current_path = os.path.dirname(__file__)
client_path = os.path.join(current_path, "client.py")
@@ -130,6 +131,8 @@ if __name__ == "__main__": # pragma: no cover
BECMotorMap,
BECDock,
BECDockArea,
SpiralProgressBar,
Ring,
]
generator = ClientGenerator()
generator.generate_client(clss)

View File

@@ -1,11 +1,12 @@
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBar
class RPCWidgetHandler:
"""Handler class for creating widgets from RPC messages."""
widget_classes = {"BECFigure": BECFigure}
widget_classes = {"BECFigure": BECFigure, "SpiralProgressBar": SpiralProgressBar}
@staticmethod
def create_widget(widget_type, **kwargs) -> BECConnector:

View File

@@ -6,12 +6,13 @@ import h5py
import numpy as np
import pyqtgraph as pg
import zmq
from pyqtgraph.Qt import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import QDialog, QFileDialog, QFrame, QLabel, QShortcut, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
# from scipy.stats import multivariate_normal
@@ -23,7 +24,7 @@ class EigerPlot(QWidget):
# pg.setConfigOptions(background="w", foreground="k", antialias=True)
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "eiger_plot.ui"), self)
self.ui = UILoader().load_ui(os.path.join(current_path, "eiger_plot.ui"), self)
# Set widow name
self.setWindowTitle("Eiger Plot")
@@ -60,19 +61,22 @@ class EigerPlot(QWidget):
self.update_hist()
# Adding Items to Graphical Layout
self.glw_layout = QVBoxLayout(self.ui.glw_placeholder)
self.glw = pg.GraphicsLayoutWidget()
self.glw_layout.addWidget(self.glw)
self.glw.addItem(self.plot_item)
self.glw.addItem(self.hist)
def hook_signals(self):
# Buttons
# self.pushButton_test.clicked.connect(self.start_sim_stream)
self.pushButton_mask.clicked.connect(self.load_mask_dialog)
self.pushButton_delete_mask.clicked.connect(self.delete_mask)
self.pushButton_help.clicked.connect(self.show_help_dialog)
self.ui.pushButton_mask.clicked.connect(self.load_mask_dialog)
self.ui.pushButton_delete_mask.clicked.connect(self.delete_mask)
self.ui.pushButton_help.clicked.connect(self.show_help_dialog)
# SpinBoxes
self.doubleSpinBox_hist_min.valueChanged.connect(self.update_hist)
self.doubleSpinBox_hist_max.valueChanged.connect(self.update_hist)
self.ui.doubleSpinBox_hist_min.valueChanged.connect(self.update_hist)
self.ui.doubleSpinBox_hist_max.valueChanged.connect(self.update_hist)
# Signal/Slots
self.update_signal.connect(self.on_image_update)
@@ -81,47 +85,47 @@ class EigerPlot(QWidget):
# Key bindings for rotation
rotate_plus = QShortcut(QKeySequence("Ctrl+A"), self)
rotate_minus = QShortcut(QKeySequence("Ctrl+Z"), self)
self.comboBox_rotation.setToolTip("Increase rotation: Ctrl+A\nDecrease rotation: Ctrl+Z")
self.checkBox_transpose.setToolTip("Toggle transpose: Ctrl+T")
self.ui.comboBox_rotation.setToolTip("Increase rotation: Ctrl+A\nDecrease rotation: Ctrl+Z")
self.ui.checkBox_transpose.setToolTip("Toggle transpose: Ctrl+T")
max_index = self.comboBox_rotation.count() - 1 # Maximum valid index
max_index = self.ui.comboBox_rotation.count() - 1 # Maximum valid index
rotate_plus.activated.connect(
lambda: self.comboBox_rotation.setCurrentIndex(
min(self.comboBox_rotation.currentIndex() + 1, max_index)
lambda: self.ui.comboBox_rotation.setCurrentIndex(
min(self.ui.comboBox_rotation.currentIndex() + 1, max_index)
)
)
rotate_minus.activated.connect(
lambda: self.comboBox_rotation.setCurrentIndex(
max(self.comboBox_rotation.currentIndex() - 1, 0)
lambda: self.ui.comboBox_rotation.setCurrentIndex(
max(self.ui.comboBox_rotation.currentIndex() - 1, 0)
)
)
# Key bindings for transpose
transpose = QShortcut(QKeySequence("Ctrl+T"), self)
transpose.activated.connect(self.checkBox_transpose.toggle)
transpose.activated.connect(self.ui.checkBox_transpose.toggle)
FFT = QShortcut(QKeySequence("Ctrl+F"), self)
FFT.activated.connect(self.checkBox_FFT.toggle)
self.checkBox_FFT.setToolTip("Toggle FFT: Ctrl+F")
FFT.activated.connect(self.ui.checkBox_FFT.toggle)
self.ui.checkBox_FFT.setToolTip("Toggle FFT: Ctrl+F")
log = QShortcut(QKeySequence("Ctrl+L"), self)
log.activated.connect(self.checkBox_log.toggle)
self.checkBox_log.setToolTip("Toggle log: Ctrl+L")
log.activated.connect(self.ui.checkBox_log.toggle)
self.ui.checkBox_log.setToolTip("Toggle log: Ctrl+L")
mask = QShortcut(QKeySequence("Ctrl+M"), self)
mask.activated.connect(self.pushButton_mask.click)
self.pushButton_mask.setToolTip("Load mask: Ctrl+M")
mask.activated.connect(self.ui.pushButton_mask.click)
self.ui.pushButton_mask.setToolTip("Load mask: Ctrl+M")
delete_mask = QShortcut(QKeySequence("Ctrl+D"), self)
delete_mask.activated.connect(self.pushButton_delete_mask.click)
self.pushButton_delete_mask.setToolTip("Delete mask: Ctrl+D")
delete_mask.activated.connect(self.ui.pushButton_delete_mask.click)
self.ui.pushButton_delete_mask.setToolTip("Delete mask: Ctrl+D")
def update_hist(self):
self.hist_levels = [
self.doubleSpinBox_hist_min.value(),
self.doubleSpinBox_hist_max.value(),
self.ui.doubleSpinBox_hist_min.value(),
self.ui.doubleSpinBox_hist_max.value(),
]
self.hist.setLevels(min=self.hist_levels[0], max=self.hist_levels[1])
self.hist.setHistogramRange(
@@ -160,16 +164,18 @@ class EigerPlot(QWidget):
# self.image = np.ma.masked_array(self.image, mask=self.mask) #TODO test if np works
self.image = self.image * (1 - self.mask) + 1
if self.checkBox_FFT.isChecked():
if self.ui.checkBox_FFT.isChecked():
self.image = np.abs(np.fft.fftshift(np.fft.fft2(self.image)))
if self.comboBox_rotation.currentIndex() > 0: # rotate
self.image = np.rot90(self.image, k=self.comboBox_rotation.currentIndex(), axes=(0, 1))
if self.ui.comboBox_rotation.currentIndex() > 0: # rotate
self.image = np.rot90(
self.image, k=self.ui.comboBox_rotation.currentIndex(), axes=(0, 1)
)
if self.checkBox_transpose.isChecked(): # transpose
if self.ui.checkBox_transpose.isChecked(): # transpose
self.image = np.transpose(self.image)
if self.checkBox_log.isChecked():
if self.ui.checkBox_log.isChecked():
self.image = np.log10(self.image)
self.imageItem.setImage(self.image, autoLevels=False)

View File

@@ -13,7 +13,7 @@
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="1,4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
@@ -191,17 +191,10 @@
</layout>
</item>
<item>
<widget class="GraphicsLayoutWidget" name="glw"/>
<widget class="QWidget" name="glw_placeholder" native="true"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsLayoutWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -2,7 +2,7 @@ import os
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, uic
from pyqtgraph.Qt import QtWidgets
from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtpy.QtCore import QSize
@@ -10,9 +10,10 @@ from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher
from bec_widgets.utils import BECDispatcher, UILoader
from bec_widgets.widgets import BECFigure
from bec_widgets.widgets.dock.dock_area import BECDockArea
from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBar
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
@@ -39,11 +40,11 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
super().__init__(parent)
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "jupyter_console_window.ui"), self)
self.ui = UILoader().load_ui(os.path.join(current_path, "jupyter_console_window.ui"), self)
self._init_ui()
self.splitter.setSizes([200, 100])
self.ui.splitter.setSizes([200, 100])
self.safe_close = False
# self.figure.clean_signal.connect(self.confirm_close)
@@ -62,6 +63,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
"d1": self.d1,
"d2": self.d2,
"d3": self.d3,
"bar": self.bar,
"b2a": self.button_2_a,
"b2b": self.button_2_b,
"b2c": self.button_2_c,
@@ -73,11 +75,11 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
def _init_ui(self):
# Plotting window
self.glw_1_layout = QVBoxLayout(self.glw) # Create a new QVBoxLayout
self.glw_1_layout = QVBoxLayout(self.ui.glw) # Create a new QVBoxLayout
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
self.dock_layout = QVBoxLayout(self.dock_placeholder)
self.dock_layout = QVBoxLayout(self.ui.dock_placeholder)
self.dock = BECDockArea(gui_id="remote")
self.dock_layout.addWidget(self.dock)
@@ -87,7 +89,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
# init dock for testing
self._init_dock()
self.console_layout = QVBoxLayout(self.widget_console)
self.console_layout = QVBoxLayout(self.ui.widget_console)
self.console = JupyterConsoleWidget()
self.console_layout.addWidget(self.console)
self.console.set_default_style("linux")
@@ -114,14 +116,14 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.button_2_b = QtWidgets.QPushButton("button after without postions specified")
self.button_2_c = QtWidgets.QPushButton("button super late")
self.button_3 = QtWidgets.QPushButton("Button above Figure ")
self.label_1 = QtWidgets.QLabel("some scan info label with useful information")
self.bar = SpiralProgressBar()
self.label_2 = QtWidgets.QLabel("label which is added separately")
self.label_3 = QtWidgets.QLabel("Label above figure")
self.d1 = self.dock.add_dock(widget=self.button_1, position="left")
self.d1.addWidget(self.label_2)
self.d2 = self.dock.add_dock(widget=self.label_1, position="right")
self.d2 = self.dock.add_dock(widget=self.bar, position="right")
self.d3 = self.dock.add_dock(name="figure")
self.fig_dock3 = BECFigure()
self.fig_dock3.plot(x_name="samx", y_name="bpm4d")

View File

@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1433</width>
<height>689</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Plot Config 2</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="BECMonitor" name="plot_1"/>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="pushButton_setting_2">
<property name="text">
<string>Setting Plot 2</string>
</property>
</widget>
</item>
<item row="3" column="2" colspan="2">
<widget class="BECMonitor" name="plot_2"/>
</item>
<item row="1" column="4">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Plot Scan Types = True</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_setting_1">
<property name="text">
<string>Setting Plot 1</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Plot Config 1</string>
</property>
</widget>
</item>
<item row="1" column="5">
<widget class="QPushButton" name="pushButton_setting_3">
<property name="text">
<string>Setting Plot 3</string>
</property>
</widget>
</item>
<item row="3" column="4" colspan="2">
<widget class="BECMonitor" name="plot_3"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1433</width>
<height>37</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<customwidgets>
<customwidget>
<class>BECMonitor</class>
<extends>QGraphicsView</extends>
<header location="global">bec_widgets.widgets.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -1,197 +0,0 @@
import os
from qtpy import uic
from qtpy.QtWidgets import QApplication, QMainWindow
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets import BECMonitor
# some default configs for demonstration purposes
CONFIG_SIMPLE = {
"plot_settings": {
"background_color": "black",
"num_columns": 2,
"colormap": "plasma",
"scan_types": False,
},
"plot_data": [
{
"plot_name": "BPM4i plots vs samx",
"x_label": "Motor X",
"y_label": "bpm4i",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx"}],
"y": [{"name": "bpm4i", "entry": "bpm4i"}],
},
},
# {
# "type": "history",
# "signals": {
# "x": [{"name": "samx"}],
# "y": [{"name": "bpm4i", "entry": "bpm4i"}],
# },
# },
# {
# "type": "dap",
# 'worker':'some_worker',
# "signals": {
# "x": [{"name": "samx"}],
# "y": [{"name": "bpm4i", "entry": "bpm4i"}],
# },
# },
],
},
{
"plot_name": "Gauss plots vs samx",
"x_label": "Motor X",
"y_label": "Gauss",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "gauss_bpm"}, {"name": "gauss_adc1"}],
},
}
],
},
],
}
CONFIG_SCAN_MODE = {
"plot_settings": {
"background_color": "white",
"num_columns": 3,
"colormap": "plasma",
"scan_types": True,
},
"plot_data": {
"grid_scan": [
{
"plot_name": "Grid plot 1",
"x_label": "Motor X",
"y_label": "BPM",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "gauss_bpm"}],
},
}
],
},
{
"plot_name": "Grid plot 2",
"x_label": "Motor X",
"y_label": "BPM",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "gauss_adc1"}],
},
}
],
},
{
"plot_name": "Grid plot 3",
"x_label": "Motor X",
"y_label": "BPM",
"sources": [
{
"type": "scan_segment",
"signals": {"x": [{"name": "samy"}], "y": [{"name": "gauss_adc2"}]},
}
],
},
{
"plot_name": "Grid plot 4",
"x_label": "Motor X",
"y_label": "BPM",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samy", "entry": "samy"}],
"y": [{"name": "gauss_adc3"}],
},
}
],
},
],
"line_scan": [
{
"plot_name": "BPM plots vs samx",
"x_label": "Motor X",
"y_label": "Gauss",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "bpm4i"}],
},
}
],
},
{
"plot_name": "Gauss plots vs samx",
"x_label": "Motor X",
"y_label": "Gauss",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "gauss_bpm"}, {"name": "gauss_adc1"}],
},
}
],
},
],
},
}
class ModularApp(QMainWindow):
def __init__(self, client=None, parent=None):
super(ModularApp, self).__init__(parent)
# Client and device manager from BEC
self.client = BECDispatcher().client if client is None else client
# Loading UI
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "modular.ui"), self)
self._init_plots()
def _init_plots(self):
"""Initialize plots and connect the buttons to the config dialogs"""
plots = [self.plot_1, self.plot_2, self.plot_3]
configs = [CONFIG_SIMPLE, CONFIG_SCAN_MODE, CONFIG_SCAN_MODE]
buttons = [self.pushButton_setting_1, self.pushButton_setting_2, self.pushButton_setting_3]
# hook plots, configs and buttons together
for plot, config, button in zip(plots, configs, buttons):
plot.on_config_update(config)
button.clicked.connect(plot.show_config_dialog)
if __name__ == "__main__":
# BECclient global variables
client = BECDispatcher().client
client.start()
app = QApplication([])
modularApp = ModularApp(client=client)
window = modularApp
window.show()
app.exec()

View File

@@ -151,7 +151,7 @@ class MotorControlPanel(QWidget):
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
# Set the window to a fixed size based on its contents
self.layout().setSizeConstraint(layout.SetFixedSize)
# self.layout().setSizeConstraint(layout.SetFixedSize)
class MotorControlPanelAbsolute(QWidget):
@@ -178,9 +178,6 @@ class MotorControlPanelAbsolute(QWidget):
# Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
# Set the window to a fixed size based on its contents
self.layout().setSizeConstraint(layout.SetFixedSize)
class MotorControlPanelRelative(QWidget):
def __init__(self, parent=None, client=None, config=None):
@@ -206,9 +203,6 @@ class MotorControlPanelRelative(QWidget):
# Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
# Set the window to a fixed size based on its contents
self.layout().setSizeConstraint(layout.SetFixedSize)
if __name__ == "__main__": # pragma: no cover
import argparse

View File

@@ -29,10 +29,10 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="GraphicsLayoutWidget" name="glw_plot"/>
<widget class="GraphicsLayoutWidget" name="glw_image"/>
<widget class="QWidget" name="glw_plot_placeholder" native="true"/>
<widget class="QWidget" name="glw_image_placeholder" native="true"/>
</widget>
<widget class="QWidget" name="">
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,1,1,15">
<item>
<widget class="QPushButton" name="pushButton_generate">
@@ -143,13 +143,6 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsLayoutWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -9,18 +9,17 @@ from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.redis_connector import RedisConnector
from pyqtgraph import mkBrush, mkPen
from pyqtgraph.Qt import QtCore, QtWidgets, uic
from pyqtgraph.Qt.QtCore import pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QTableWidgetItem
from pyqtgraph.Qt import QtCore, QtWidgets
from qtpy.QtCore import Signal, Slot
from qtpy.QtWidgets import QTableWidgetItem, QVBoxLayout
from bec_widgets.utils import Colors, Crosshair
from bec_widgets.utils import Colors, Crosshair, UILoader
from bec_widgets.utils.bec_dispatcher import BECDispatcher
class StreamPlot(QtWidgets.QWidget):
update_signal = pyqtSignal()
roi_signal = pyqtSignal(tuple)
update_signal = Signal()
roi_signal = Signal(tuple)
def __init__(self, name="", y_value_list=["gauss_bpm"], client=None, parent=None) -> None:
"""
@@ -39,7 +38,7 @@ class StreamPlot(QtWidgets.QWidget):
pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k")
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "line_plot.ui"), self)
self.ui = UILoader().load_ui(os.path.join(current_path, "line_plot.ui"), self)
self._idle_time = 100
self.connector = RedisConnector(["localhost:6379"])
@@ -82,6 +81,9 @@ class StreamPlot(QtWidgets.QWidget):
# LabelItem for ROI
self.label_plot = pg.LabelItem(justify="center")
self.glw_plot_layout = QVBoxLayout(self.ui.glw_plot_placeholder)
self.glw_plot = pg.GraphicsLayoutWidget()
self.glw_plot_layout.addWidget(self.glw_plot)
self.glw_plot.addItem(self.label_plot)
self.label_plot.setText("ROI region")
@@ -112,6 +114,9 @@ class StreamPlot(QtWidgets.QWidget):
# Label for coordinates moved
self.label_image_moved = pg.LabelItem(justify="center")
self.glw_image_layout = QVBoxLayout(self.ui.glw_image_placeholder)
self.glw_image = pg.GraphicsLayoutWidget()
self.glw_plot_layout.addWidget(self.glw_image)
self.glw_image.addItem(self.label_image_moved)
self.label_image_moved.setText("Actual coordinates (X, Y)")
@@ -221,10 +226,10 @@ class StreamPlot(QtWidgets.QWidget):
def init_table(self):
# Init number of rows in table according to n of devices
self.cursor_table.setRowCount(len(self.y_value_list))
self.ui.cursor_table.setRowCount(len(self.y_value_list))
# self.table.setHorizontalHeaderLabels(["(X, Y) - Moved", "(X, Y) - Clicked"]) #TODO can be dynamic
self.cursor_table.setVerticalHeaderLabels(self.y_value_list)
self.cursor_table.resizeColumnsToContents()
self.ui.cursor_table.setVerticalHeaderLabels(self.y_value_list)
self.ui.cursor_table.resizeColumnsToContents()
def update_table(self, table_widget, x, y_values):
for i, y in enumerate(y_values):
@@ -287,13 +292,13 @@ class StreamPlot(QtWidgets.QWidget):
self.update_signal.emit()
@pyqtSlot(dict, dict)
@Slot(dict, dict)
def on_dap_update(self, data: dict, metadata: dict):
flipped_data = self.flip_even_rows(data["data"]["z"])
self.img.setImage(flipped_data)
@pyqtSlot(dict, dict)
@Slot(dict, dict)
def new_proj(self, content: dict, _metadata: dict):
proj_nr = content["signals"]["proj_nr"]
endpoint = f"px_stream/projection_{proj_nr}/metadata"

View File

@@ -0,0 +1,17 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
"""PySide6 port of the Qt Designer taskmenuextension example from Qt v6.x"""
import sys
from bec_ipython_client.main import BECIPythonClient
from PySide6.QtWidgets import QApplication
from tictactoe import TicTacToe
if __name__ == "__main__":
app = QApplication(sys.argv)
window = TicTacToe()
window.state = "-X-XO----"
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,24 @@
import os
import subprocess
import sys
from PySide6.scripts.pyside_tool import designer
import bec_widgets
def main():
# os.environ["PYSIDE_DESIGNER_PLUGINS"] = os.path.join(
# "/Users/janwyzula/PSI/bec_widgets/bec_widgets/plugin"
# )
os.environ["PYSIDE_DESIGNER_PLUGINS"] = os.path.join(
os.path.dirname(bec_widgets.__file__), "widgets/motor_control/selection"
)
# os.environ["PYTHONFRAMEWORKPREFIX"] = os.path.join(
# os.path.dirname(bec_widgets.__file__), "widgets/motor_control/selection"
# )
designer()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,12 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from tictactoe import TicTacToe
from tictactoeplugin import TicTacToePlugin
# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin
if __name__ == "__main__":
QPyDesignerCustomWidgetCollection.addCustomWidget(TicTacToePlugin())

View File

@@ -0,0 +1,4 @@
{
"files": ["tictactoe.py", "main.py", "registertictactoe.py", "tictactoeplugin.py",
"tictactoetaskmenu.py"]
}

View File

@@ -0,0 +1,135 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import Property, QPoint, QRect, QSize, Qt, Slot
from PySide6.QtGui import QPainter, QPen
from PySide6.QtWidgets import QWidget
EMPTY = "-"
CROSS = "X"
NOUGHT = "O"
DEFAULT_STATE = "---------"
class TicTacToe(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._state = DEFAULT_STATE
self._turn_number = 0
def minimumSizeHint(self):
return QSize(200, 200)
def sizeHint(self):
return QSize(200, 200)
def setState(self, new_state):
self._turn_number = 0
self._state = DEFAULT_STATE
for position in range(min(9, len(new_state))):
mark = new_state[position]
if mark == CROSS or mark == NOUGHT:
self._turn_number += 1
self._change_state_at(position, mark)
position += 1
self.update()
def state(self):
return self._state
@Slot()
def clear_board(self):
self._state = DEFAULT_STATE
self._turn_number = 0
self.update()
def _change_state_at(self, pos, new_state):
self._state = self._state[:pos] + new_state + self._state[pos + 1 :]
def mousePressEvent(self, event):
if self._turn_number == 9:
self.clear_board()
return
for position in range(9):
cell = self._cell_rect(position)
if cell.contains(event.position().toPoint()):
if self._state[position] == EMPTY:
new_state = CROSS if self._turn_number % 2 == 0 else NOUGHT
self._change_state_at(position, new_state)
self._turn_number += 1
self.update()
def paintEvent(self, event):
with QPainter(self) as painter:
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(Qt.darkGreen, 1))
painter.drawLine(self._cell_width(), 0, self._cell_width(), self.height())
painter.drawLine(2 * self._cell_width(), 0, 2 * self._cell_width(), self.height())
painter.drawLine(0, self._cell_height(), self.width(), self._cell_height())
painter.drawLine(0, 2 * self._cell_height(), self.width(), 2 * self._cell_height())
painter.setPen(QPen(Qt.darkBlue, 2))
for position in range(9):
cell = self._cell_rect(position)
if self._state[position] == CROSS:
painter.drawLine(cell.topLeft(), cell.bottomRight())
painter.drawLine(cell.topRight(), cell.bottomLeft())
elif self._state[position] == NOUGHT:
painter.drawEllipse(cell)
painter.setPen(QPen(Qt.yellow, 3))
for position in range(0, 8, 3):
if (
self._state[position] != EMPTY
and self._state[position + 1] == self._state[position]
and self._state[position + 2] == self._state[position]
):
y = self._cell_rect(position).center().y()
painter.drawLine(0, y, self.width(), y)
self._turn_number = 9
for position in range(3):
if (
self._state[position] != EMPTY
and self._state[position + 3] == self._state[position]
and self._state[position + 6] == self._state[position]
):
x = self._cell_rect(position).center().x()
painter.drawLine(x, 0, x, self.height())
self._turn_number = 9
if (
self._state[0] != EMPTY
and self._state[4] == self._state[0]
and self._state[8] == self._state[0]
):
painter.drawLine(0, 0, self.width(), self.height())
self._turn_number = 9
if (
self._state[2] != EMPTY
and self._state[4] == self._state[2]
and self._state[6] == self._state[2]
):
painter.drawLine(0, self.height(), self.width(), 0)
self._turn_number = 9
def _cell_rect(self, position):
h_margin = self.width() / 30
v_margin = self.height() / 30
row = int(position / 3)
column = position - 3 * row
pos = QPoint(column * self._cell_width() + h_margin, row * self._cell_height() + v_margin)
size = QSize(self._cell_width() - 2 * h_margin, self._cell_height() - 2 * v_margin)
return QRect(pos, size)
def _cell_width(self):
return self.width() / 3
def _cell_height(self):
return self.height() / 3
state = Property(str, state, setState)

View File

@@ -0,0 +1,68 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtDesigner import QDesignerCustomWidgetInterface
from PySide6.QtGui import QIcon
from tictactoe import TicTacToe
from tictactoetaskmenu import TicTacToeTaskMenuFactory
DOM_XML = """
<ui language='c++'>
<widget class='TicTacToe' name='ticTacToe'>
<property name='geometry'>
<rect>
<x>0</x>
<y>0</y>
<width>200</width>
<height>200</height>
</rect>
</property>
<property name='state'>
<string>-X-XO----</string>
</property>
</widget>
</ui>
"""
class TicTacToePlugin(QDesignerCustomWidgetInterface):
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = TicTacToe(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "tictactoe"
def initialize(self, form_editor):
self._form_editor = form_editor
manager = form_editor.extensionManager()
iid = TicTacToeTaskMenuFactory.task_menu_iid()
manager.registerExtensions(TicTacToeTaskMenuFactory(manager), iid)
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "TicTacToe"
def toolTip(self):
return "Tic Tac Toe Example, demonstrating class QDesignerTaskMenuExtension (Python)"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,67 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import Slot
from PySide6.QtDesigner import QExtensionFactory, QPyDesignerTaskMenuExtension
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
from tictactoe import TicTacToe
class TicTacToeDialog(QDialog):
def __init__(self, parent):
super().__init__(parent)
layout = QVBoxLayout(self)
self._ticTacToe = TicTacToe(self)
layout.addWidget(self._ticTacToe)
button_box = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
reset_button = button_box.button(QDialogButtonBox.Reset)
reset_button.clicked.connect(self._ticTacToe.clear_board)
layout.addWidget(button_box)
def set_state(self, new_state):
self._ticTacToe.setState(new_state)
def state(self):
return self._ticTacToe.state
class TicTacToeTaskMenu(QPyDesignerTaskMenuExtension):
def __init__(self, ticTacToe, parent):
super().__init__(parent)
self._ticTacToe = ticTacToe
self._edit_state_action = QAction("Edit State...", None)
self._edit_state_action.triggered.connect(self._edit_state)
def taskActions(self):
return [self._edit_state_action]
def preferredEditAction(self):
return self._edit_state_action
@Slot()
def _edit_state(self):
dialog = TicTacToeDialog(self._ticTacToe)
dialog.set_state(self._ticTacToe.state)
if dialog.exec() == QDialog.Accepted:
self._ticTacToe.state = dialog.state()
class TicTacToeTaskMenuFactory(QExtensionFactory):
def __init__(self, extension_manager):
super().__init__(extension_manager)
@staticmethod
def task_menu_iid():
return "org.qt-project.Qt.Designer.TaskMenu"
def createExtension(self, object, iid, parent):
if iid != TicTacToeTaskMenuFactory.task_menu_iid():
return None
if object.__class__.__name__ != "TicTacToe":
return None
return TicTacToeTaskMenu(object, parent)

View File

@@ -7,4 +7,5 @@ from .crosshair import Crosshair
from .entry_validator import EntryValidator
from .layout_manager import GridLayoutManager
from .rpc_decorator import register_rpc_methods, rpc_public
from .ui_loader import UILoader
from .validator_delegate import DoubleValidationDelegate

View File

@@ -8,13 +8,13 @@ from qtpy.QtCore import Signal as pyqtSignal
class Crosshair(QObject):
# Signal for 1D plot
coordinatesChanged1D = pyqtSignal(float, list)
coordinatesClicked1D = pyqtSignal(float, list)
coordinatesChanged1D = pyqtSignal(tuple)
coordinatesClicked1D = pyqtSignal(tuple)
# Signal for 2D plot
coordinatesChanged2D = pyqtSignal(float, float)
coordinatesClicked2D = pyqtSignal(float, float)
coordinatesChanged2D = pyqtSignal(tuple)
coordinatesClicked2D = pyqtSignal(tuple)
def __init__(self, plot_item: pg.PlotItem, precision: int = None, parent=None):
def __init__(self, plot_item: pg.PlotItem, precision: int = 3, parent=None):
"""
Crosshair for 1D and 2D plots.
@@ -174,10 +174,11 @@ class Crosshair(QObject):
if isinstance(item, pg.PlotDataItem):
if x is None or all(v is None for v in y_values):
return
self.coordinatesChanged1D.emit(
coordinance_to_emit = (
round(x, self.precision),
[round(y_val, self.precision) for y_val in y_values],
)
self.coordinatesChanged1D.emit(coordinance_to_emit)
for i, y_val in enumerate(y_values):
self.marker_moved_1d[i].setData(
[x if not self.is_log_x else np.log10(x)],
@@ -186,7 +187,8 @@ class Crosshair(QObject):
elif isinstance(item, pg.ImageItem):
if x is None or y_values is None:
return
self.coordinatesChanged2D.emit(x, y_values)
coordinance_to_emit = (x, y_values)
self.coordinatesChanged2D.emit(coordinance_to_emit)
def mouse_clicked(self, event):
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.
@@ -209,10 +211,11 @@ class Crosshair(QObject):
if isinstance(item, pg.PlotDataItem):
if x is None or all(v is None for v in y_values):
return
self.coordinatesClicked1D.emit(
coordinate_to_emit = (
round(x, self.precision),
[round(y_val, self.precision) for y_val in y_values],
)
self.coordinatesClicked1D.emit(coordinate_to_emit)
for i, y_val in enumerate(y_values):
for marker in self.marker_clicked_1d[i]:
marker.setData(
@@ -222,7 +225,8 @@ class Crosshair(QObject):
elif isinstance(item, pg.ImageItem):
if x is None or y_values is None:
return
self.coordinatesClicked2D.emit(x, y_values)
coordinate_to_emit = (x, y_values)
self.coordinatesClicked2D.emit(coordinate_to_emit)
self.marker_2d.setPos([x, y_values])
def check_log(self):

View File

@@ -0,0 +1,58 @@
from qtpy import QT_VERSION
from qtpy.QtCore import QFile, QIODevice
class UILoader:
"""Universal UI loader for PyQt5, PyQt6, PySide2, and PySide6."""
def __init__(self, parent=None):
self.parent = parent
if QT_VERSION.startswith("5"):
# PyQt5 or PySide2
from qtpy import uic
self.loader = uic.loadUi
elif QT_VERSION.startswith("6"):
# PyQt6 or PySide6
try:
from PySide6.QtUiTools import QUiLoader
self.loader = self.load_ui_pyside6
except ImportError:
from PyQt6.uic import loadUi
self.loader = loadUi
def load_ui_pyside6(self, ui_file, parent=None):
"""
Specific loader for PySide6 using QUiLoader.
Args:
ui_file(str): Path to the .ui file.
parent(QWidget): Parent widget.
Returns:
QWidget: The loaded widget.
"""
from PySide6.QtUiTools import QUiLoader
loader = QUiLoader(parent)
file = QFile(ui_file)
if not file.open(QIODevice.ReadOnly):
raise IOError(f"Cannot open file: {ui_file}")
widget = loader.load(file, parent)
file.close()
return widget
def load_ui(self, ui_file, parent=None):
"""
Universal UI loader method.
Args:
ui_file(str): Path to the .ui file.
parent(QWidget): Parent widget.
Returns:
QWidget: The loaded widget.
"""
if parent is None:
parent = self.parent
return self.loader(ui_file, parent)

View File

@@ -1,3 +1,4 @@
from .dock import BECDock, BECDockArea
from .figure import BECFigure, FigureConfig
from .scan_control import ScanControl
from .spiral_progress_bar import SpiralProgressBar

View File

@@ -0,0 +1,14 @@
from qtpy.QtWidgets import QComboBox
from bec_widgets.utils import BECConnector, ConnectionConfig
class DeviceCombobox(BECConnector, QComboBox):
def __init__(self, parent=None, client=None, config=None, gui_id=None):
super().__init__(client=client, config=config, gui_id=gui_id)
QComboBox.__init__(self, parent=parent)
self.get_bec_shortcuts()
def get_device(self):
return getattr(self.dev, self.text().lower())

View File

@@ -16,6 +16,7 @@ from qtpy.QtWidgets import (
QTableWidgetItem,
)
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
@@ -37,25 +38,25 @@ class MotorCoordinateTable(MotorControlWidget):
def _load_ui(self):
"""Load the UI for the coordinate table."""
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "motor_table.ui"), self)
self.ui = UILoader().load_ui(os.path.join(current_path, "motor_table.ui"), self)
def _init_ui(self):
"""Initialize the UI"""
# Setup table behaviour
self._setup_table()
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.ui.table.setSelectionBehavior(QTableWidget.SelectRows)
# for tag columns default tag
self.tag_counter = 1
# Connect signals and slots
self.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto)
self.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
self.ui.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto)
self.ui.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
# Keyboard shortcuts for deleting a row
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.table)
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.ui.table)
self.delete_shortcut.activated.connect(self.delete_selected_row)
self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.table)
self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.ui.table)
self.backspace_shortcut.activated.connect(self.delete_selected_row)
# Warning message for mode switch enable/disable
@@ -83,13 +84,13 @@ class MotorCoordinateTable(MotorControlWidget):
self.mode = self.config["motor_control"].get("mode", "Individual")
# Set combobox to default mode
self.comboBox_mode.setCurrentText(self.mode)
self.ui.comboBox_mode.setCurrentText(self.mode)
self._init_ui()
def _setup_table(self):
"""Setup the table with appropriate headers and configurations."""
mode = self.comboBox_mode.currentText()
mode = self.ui.comboBox_mode.currentText()
if mode == "Individual":
self._setup_individual_mode()
@@ -101,14 +102,14 @@ class MotorCoordinateTable(MotorControlWidget):
def _setup_individual_mode(self):
"""Setup the table for individual mode."""
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"])
self.table.verticalHeader().setVisible(False)
self.ui.table.setColumnCount(5)
self.ui.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"])
self.ui.table.verticalHeader().setVisible(False)
def _setup_start_stop_mode(self):
"""Setup the table for start/stop mode."""
self.table.setColumnCount(8)
self.table.setHorizontalHeaderLabels(
self.ui.table.setColumnCount(8)
self.ui.table.setHorizontalHeaderLabels(
[
"Show",
"Move [start]",
@@ -120,15 +121,15 @@ class MotorCoordinateTable(MotorControlWidget):
"Y [end]",
]
)
self.table.verticalHeader().setVisible(False)
self.ui.table.verticalHeader().setVisible(False)
# Set flag to track if the coordinate is stat or the end of the entry
self.is_next_entry_end = False
def mode_switch(self):
"""Switch between individual and start/stop mode."""
last_selected_index = self.comboBox_mode.currentIndex()
last_selected_index = self.ui.comboBox_mode.currentIndex()
if self.table.rowCount() > 0 and self.warning_message is True:
if self.ui.table.rowCount() > 0 and self.warning_message is True:
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Critical)
msgBox.setText(
@@ -138,9 +139,9 @@ class MotorCoordinateTable(MotorControlWidget):
returnValue = msgBox.exec()
if returnValue is QMessageBox.Cancel:
self.comboBox_mode.blockSignals(True) # Block signals
self.comboBox_mode.setCurrentIndex(last_selected_index)
self.comboBox_mode.blockSignals(False) # Unblock signals
self.ui.comboBox_mode.blockSignals(True) # Block signals
self.ui.comboBox_mode.setCurrentIndex(last_selected_index)
self.ui.comboBox_mode.blockSignals(False) # Unblock signals
return
# Wipe table
@@ -170,7 +171,7 @@ class MotorCoordinateTable(MotorControlWidget):
y(float): Y coordinate.
"""
mode = self.comboBox_mode.currentText()
mode = self.ui.comboBox_mode.currentText()
if mode == "Individual":
checkbox_pos = 0
button_pos = 1
@@ -181,8 +182,8 @@ class MotorCoordinateTable(MotorControlWidget):
color = "green"
# Add new row -> new entry
row_count = self.table.rowCount()
self.table.insertRow(row_count)
row_count = self.ui.table.rowCount()
self.ui.table.insertRow(row_count)
# Add Widgets
self._add_widgets(
@@ -213,8 +214,8 @@ class MotorCoordinateTable(MotorControlWidget):
color = "blue"
# Add new row -> new entry
row_count = self.table.rowCount()
self.table.insertRow(row_count)
row_count = self.ui.table.rowCount()
self.ui.table.insertRow(row_count)
# Add Widgets
self._add_widgets(
@@ -236,7 +237,7 @@ class MotorCoordinateTable(MotorControlWidget):
elif self.is_next_entry_end is True: # It is the end position of the entry
print("End position")
row_count = self.table.rowCount() - 1 # Current row
row_count = self.ui.table.rowCount() - 1 # Current row
button_pos = 2
x_pos = 6
y_pos = 7
@@ -294,7 +295,7 @@ class MotorCoordinateTable(MotorControlWidget):
# Add widgets
self._add_checkbox(row, checkBox_pos, x_pos, y_pos)
self._add_move_button(row, button_pos, x_pos, y_pos)
self.table.setItem(row, tag_pos, QTableWidgetItem(tag))
self.ui.table.setItem(row, tag_pos, QTableWidgetItem(tag))
self._add_line_edit(x, row, x_pos, x_pos, y_pos, coordinate_reference, color)
self._add_line_edit(y, row, y_pos, x_pos, y_pos, coordinate_reference, color)
@@ -302,10 +303,10 @@ class MotorCoordinateTable(MotorControlWidget):
self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
# Connect item edit to emit coordinates
self.table.itemChanged.connect(
self.ui.table.itemChanged.connect(
lambda: print(f"item changed from {coordinate_reference} slot \n {x}-{y}-{color}")
)
self.table.itemChanged.connect(
self.ui.table.itemChanged.connect(
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
)
@@ -321,7 +322,7 @@ class MotorCoordinateTable(MotorControlWidget):
show_checkbox = QCheckBox()
show_checkbox.setChecked(True)
show_checkbox.stateChanged.connect(lambda: self.emit_plot_coordinates(x_pos, y_pos))
self.table.setCellWidget(row, checkBox_pos, show_checkbox)
self.ui.table.setCellWidget(row, checkBox_pos, show_checkbox)
def _add_move_button(self, row: int, button_pos: int, x_pos: int, y_pos: int) -> None:
"""
@@ -334,7 +335,7 @@ class MotorCoordinateTable(MotorControlWidget):
"""
move_button = QPushButton("Move")
move_button.clicked.connect(lambda: self.handle_move_button_click(x_pos, y_pos))
self.table.setCellWidget(row, button_pos, move_button)
self.ui.table.setCellWidget(row, button_pos, move_button)
def _add_line_edit(
self,
@@ -367,7 +368,7 @@ class MotorCoordinateTable(MotorControlWidget):
edit.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Add line edit to the table
self.table.setCellWidget(row, line_pos, edit)
self.ui.table.setCellWidget(row, line_pos, edit)
edit.textChanged.connect(
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
)
@@ -375,10 +376,10 @@ class MotorCoordinateTable(MotorControlWidget):
def wipe_motor_map_coordinates(self):
"""Wipe the motor map coordinates."""
try:
self.table.itemChanged.disconnect() # Disconnect all previous connections
self.ui.table.itemChanged.disconnect() # Disconnect all previous connections
except TypeError:
print("No previous connections to disconnect")
self.table.setRowCount(0)
self.ui.table.setRowCount(0)
reference_tags = ["Individual", "Start", "Stop"]
for reference_tag in reference_tags:
self.plot_coordinates_signal.emit([], reference_tag, "green")
@@ -391,7 +392,7 @@ class MotorCoordinateTable(MotorControlWidget):
y_pos(int): Y position of the coordinate.
"""
button = self.sender()
row = self.table.indexAt(button.pos()).row()
row = self.ui.table.indexAt(button.pos()).row()
x = self.get_coordinate(row, x_pos)
y = self.get_coordinate(row, y_pos)
@@ -410,8 +411,8 @@ class MotorCoordinateTable(MotorControlWidget):
f"Emitting plot coordinates: x_pos={x_pos}, y_pos={y_pos}, reference_tag={reference_tag}, color={color}"
)
coordinates = []
for row in range(self.table.rowCount()):
show = self.table.cellWidget(row, 0).isChecked()
for row in range(self.ui.table.rowCount()):
show = self.ui.table.cellWidget(row, 0).isChecked()
x = self.get_coordinate(row, x_pos)
y = self.get_coordinate(row, y_pos)
@@ -427,27 +428,27 @@ class MotorCoordinateTable(MotorControlWidget):
Returns:
float: Value of the coordinate.
"""
edit = self.table.cellWidget(row, column)
edit = self.ui.table.cellWidget(row, column)
value = float(edit.text()) if edit and edit.text() != "" else None
if value:
return value
def delete_selected_row(self):
"""Delete the selected row from the table."""
selected_rows = self.table.selectionModel().selectedRows()
selected_rows = self.ui.table.selectionModel().selectedRows()
for row in selected_rows:
self.table.removeRow(row.row())
if self.comboBox_mode.currentText() == "Start/Stop":
self.ui.table.removeRow(row.row())
if self.ui.comboBox_mode.currentText() == "Start/Stop":
self.emit_plot_coordinates(x_pos=4, y_pos=5, reference_tag="Start", color="blue")
self.emit_plot_coordinates(x_pos=6, y_pos=7, reference_tag="Stop", color="red")
self.is_next_entry_end = False
elif self.comboBox_mode.currentText() == "Individual":
elif self.ui.comboBox_mode.currentText() == "Individual":
self.emit_plot_coordinates(x_pos=3, y_pos=4, reference_tag="Individual", color="green")
def resize_table_auto(self):
"""Resize the table to fit the contents."""
if self.checkBox_resize_auto.isChecked():
self.table.resizeColumnsToContents()
if self.ui.checkBox_resize_auto.isChecked():
self.ui.table.resizeColumnsToContents()
def move_motor(self, x: float, y: float) -> None:
"""

View File

@@ -1,10 +1,12 @@
import os
from qtpy.QtWidgets import QWidget
from qtpy import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget, MotorControlErrors
class MotorControlAbsolute(MotorControlWidget):
@@ -23,26 +25,26 @@ class MotorControlAbsolute(MotorControlWidget):
def _load_ui(self):
"""Load the UI from the .ui file."""
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "movement_absolute.ui"), self)
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_absolute.ui"), self)
def _init_ui(self):
"""Initialize the UI."""
# Check if there are any motors connected
if self.motor_x is None or self.motor_y is None:
self.motorControl_absolute.setEnabled(False)
self.ui.motorControl_absolute.setEnabled(False)
return
# Move to absolute coordinates
self.pushButton_go_absolute.clicked.connect(
self.ui.pushButton_go_absolute.clicked.connect(
lambda: self.move_motor_absolute(
self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()
self.ui.spinBox_absolute_x.value(), self.ui.spinBox_absolute_y.value()
)
)
self.pushButton_set.clicked.connect(self.save_absolute_coordinates)
self.pushButton_save.clicked.connect(self.save_current_coordinates)
self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
self.ui.pushButton_set.clicked.connect(self.save_absolute_coordinates)
self.ui.pushButton_save.clicked.connect(self.save_current_coordinates)
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
# Enable/Disable GUI
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
@@ -80,11 +82,11 @@ class MotorControlAbsolute(MotorControlWidget):
"""
# Disable or enable all controls within the motorControl_absolute group box
for widget in self.motorControl_absolute.findChildren(QWidget):
for widget in self.ui.motorControl_absolute.findChildren(QWidget):
widget.setEnabled(enable)
# Enable the pushButton_stop if the motor is moving
self.pushButton_stop.setEnabled(True)
self.ui.pushButton_stop.setEnabled(True)
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str):
@@ -109,8 +111,8 @@ class MotorControlAbsolute(MotorControlWidget):
"""
self.precision = precision
self.config["motor_control"]["precision"] = precision
self.spinBox_absolute_x.setDecimals(precision)
self.spinBox_absolute_y.setDecimals(precision)
self.ui.spinBox_absolute_x.setDecimals(precision)
self.ui.spinBox_absolute_y.setDecimals(precision)
def move_motor_absolute(self, x: float, y: float) -> None:
"""
@@ -122,32 +124,32 @@ class MotorControlAbsolute(MotorControlWidget):
# self._enable_motor_controls(False)
target_coordinates = (x, y)
self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates)
if self.checkBox_save_with_go.isChecked():
if self.ui.checkBox_save_with_go.isChecked():
self.save_absolute_coordinates()
def _init_keyboard_shortcuts(self):
"""Initialize the keyboard shortcuts."""
# Go absolute button
self.pushButton_go_absolute.setShortcut("Ctrl+G")
self.pushButton_go_absolute.setToolTip("Ctrl+G")
self.ui.pushButton_go_absolute.setShortcut("Ctrl+G")
self.ui.pushButton_go_absolute.setToolTip("Ctrl+G")
# Set absolute coordinates
self.pushButton_set.setShortcut("Ctrl+D")
self.pushButton_set.setToolTip("Ctrl+D")
self.ui.pushButton_set.setShortcut("Ctrl+D")
self.ui.pushButton_set.setToolTip("Ctrl+D")
# Save Current coordinates
self.pushButton_save.setShortcut("Ctrl+S")
self.pushButton_save.setToolTip("Ctrl+S")
self.ui.pushButton_save.setShortcut("Ctrl+S")
self.ui.pushButton_save.setToolTip("Ctrl+S")
# Stop Button
self.pushButton_stop.setShortcut("Ctrl+X")
self.pushButton_stop.setToolTip("Ctrl+X")
self.ui.pushButton_stop.setShortcut("Ctrl+X")
self.ui.pushButton_stop.setToolTip("Ctrl+X")
def save_absolute_coordinates(self):
"""Emit the setup coordinates from the spinboxes"""
x, y = round(self.spinBox_absolute_x.value(), self.precision), round(
self.spinBox_absolute_y.value(), self.precision
x, y = round(self.ui.spinBox_absolute_x.value(), self.precision), round(
self.ui.spinBox_absolute_y.value(), self.precision
)
self.coordinates_signal.emit((x, y))

View File

@@ -0,0 +1,30 @@
import qdarktheme
from qtpy.QtWidgets import QApplication
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets.motor_control.selection.selection import MotorControlSelection
CONFIG_DEFAULT = {
"motor_control": {
"motor_x": "samx",
"motor_y": "samy",
"step_size_x": 3,
"step_size_y": 5,
"precision": 4,
"step_x_y_same": False,
"move_with_arrows": False,
}
}
if __name__ == "__main__":
bec_dispatcher = BECDispatcher()
# BECclient global variables
client = bec_dispatcher.client
client.start()
app = QApplication([])
qdarktheme.setup_theme("auto")
motor_control = MotorControlSelection(client=client, config=CONFIG_DEFAULT)
window = motor_control
window.show()
app.exec()

View File

@@ -0,0 +1,13 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from selection import MotorControlSelection
from selectionplugin import MotorControlSelectionPlugin
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin
if __name__ == "__main__":
QPyDesignerCustomWidgetCollection.addCustomWidget(MotorControlSelectionPlugin())

View File

@@ -0,0 +1,4 @@
{
"files": ["selection.py", "motor_selection_launch.py", "registertictactoe.py", "tictactoeplugin.py",
"tictactoetaskmenu.py"]
}

View File

@@ -0,0 +1,58 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from selection import MotorControlSelection
from PySide6.QtGui import QIcon
from PySide6.QtDesigner import QDesignerCustomWidgetInterface
DOM_XML = """
<ui language='c++'>
<widget class='MotorControlSelection' name='selection'>
</widget>
</ui>
"""
class MotorControlSelectionPlugin(QDesignerCustomWidgetInterface):
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = MotorControlSelection(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "selection"
def initialize(self, form_editor):
self._form_editor = form_editor
# manager = form_editor.extensionManager()
# iid = TicTacToeTaskMenuFactory.task_menu_iid()
# manager.registerExtensions(TicTacToeTaskMenuFactory(manager), iid)
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "MotorControlSelection"
def toolTip(self):
return "MotorControl Selection Example for BEC Widgets"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1 @@
from .spiral_progress_bar import SpiralProgressBar

View File

@@ -0,0 +1,184 @@
from __future__ import annotations
from typing import Literal, Optional
from bec_lib.endpoints import EndpointInfo
from pydantic import BaseModel, Field, field_validator
from pydantic_core import PydanticCustomError
from qtpy import QtGui
from bec_widgets.utils import BECConnector, ConnectionConfig
class RingConnections(BaseModel):
slot: Literal["on_scan_progress", "on_device_readback"] = None
endpoint: EndpointInfo | str = None
@field_validator("endpoint")
def validate_endpoint(cls, v, values):
slot = values.data["slot"]
endpoint = v.endpoint if isinstance(v, EndpointInfo) else v
if slot == "on_scan_progress":
if endpoint != "scans/scan_progress":
raise PydanticCustomError(
"unsupported endpoint",
"For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'.",
{"wrong_value": v},
)
elif slot == "on_device_readback":
if not endpoint.startswith("internal/devices/readback/"):
raise PydanticCustomError(
"unsupported endpoint",
"For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'.",
{"wrong_value": v},
)
return v
class RingConfig(ConnectionConfig):
direction: int | None = Field(
-1, description="Direction of the progress bars. -1 for clockwise, 1 for counter-clockwise."
)
color: str | tuple | None = Field(
(0, 159, 227, 255),
description="Color for the progress bars. Can be tuple (R, G, B, A) or string HEX Code.",
)
background_color: str | tuple | None = Field(
(200, 200, 200, 50),
description="Background color for the progress bars. Can be tuple (R, G, B, A) or string HEX Code.",
)
index: int | None = Field(0, description="Index of the progress bar. 0 is outer ring.")
line_width: int | None = Field(5, description="Line widths for the progress bars.")
start_position: int | None = Field(
90,
description="Start position for the progress bars in degrees. Default is 90 degrees - corespons to "
"the top of the ring.",
)
min_value: int | None = Field(0, description="Minimum value for the progress bars.")
max_value: int | None = Field(100, description="Maximum value for the progress bars.")
precision: int | None = Field(3, description="Precision for the progress bars.")
update_behaviour: Literal["manual", "auto"] | None = Field(
"auto", description="Update behaviour for the progress bars."
)
connections: RingConnections | None = Field(
default_factory=RingConnections, description="Connections for the progress bars."
)
class Ring(BECConnector):
USER_ACCESS = [
"get_all_rpc",
"rpc_id",
"config_dict",
"set_value",
"set_color",
"set_background",
"set_line_width",
"set_min_max_values",
"set_start_angle",
"set_connections",
"reset_connection",
]
def __init__(
self,
parent=None,
parent_progress_widget=None,
config: RingConfig | dict | None = None,
client=None,
gui_id: Optional[str] = None,
):
if config is None:
config = RingConfig(widget_class=self.__class__.__name__)
self.config = config
else:
if isinstance(config, dict):
config = RingConfig(**config)
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id)
self.parent_progress_widget = parent_progress_widget
self.color = None
self.background_color = None
self.start_position = None
self.config = config
self.value = 0
self.RID = None
self._init_config_params()
def _init_config_params(self):
self.color = self.convert_color(self.config.color)
self.background_color = self.convert_color(self.config.background_color)
self.set_start_angle(self.config.start_position)
if self.config.connections:
self.set_connections(self.config.connections.slot, self.config.connections.endpoint)
def set_value(self, value: int | float):
self.value = round(
max(self.config.min_value, min(self.config.max_value, value)), self.config.precision
)
def set_color(self, color: str | tuple):
self.config.color = color
self.color = self.convert_color(color)
def set_background(self, color: str | tuple):
self.config.background_color = color
self.color = self.convert_color(color)
def set_line_width(self, width: int):
self.config.line_width = width
def set_min_max_values(self, min_value: int, max_value: int):
self.config.min_value = min_value
self.config.max_value = max_value
def set_start_angle(self, start_angle: int):
self.config.start_position = start_angle
self.start_position = start_angle * 16
@staticmethod
def convert_color(color):
converted_color = None
if isinstance(color, str):
converted_color = QtGui.QColor(color)
elif isinstance(color, tuple):
converted_color = QtGui.QColor(*color)
return converted_color
def set_connections(self, slot: str, endpoint: str | EndpointInfo):
if self.config.connections.endpoint == endpoint and self.config.connections.slot == slot:
return
else:
self.bec_dispatcher.disconnect_slot(
self.config.connections.slot, self.config.connections.endpoint
)
self.config.connections = RingConnections(slot=slot, endpoint=endpoint)
self.bec_dispatcher.connect_slot(getattr(self, slot), endpoint)
def reset_connection(self):
self.bec_dispatcher.disconnect_slot(
self.config.connections.slot, self.config.connections.endpoint
)
self.config.connections = RingConnections()
def on_scan_progress(self, msg, meta):
current_RID = meta.get("RID", None)
if current_RID != self.RID:
self.set_min_max_values(0, msg.get("max_value", 100))
self.set_value(msg.get("value", 0))
self.parent_progress_widget.update()
def on_device_readback(self, msg, meta):
if isinstance(self.config.connections.endpoint, EndpointInfo):
endpoint = self.config.connections.endpoint.endpoint
else:
endpoint = self.config.connections.endpoint
device = endpoint.split("/")[-1]
value = msg.get("signals").get(device).get("value")
self.set_value(value)
self.parent_progress_widget.update()
def cleanup(self):
self.reset_connection()
super().cleanup()

View File

@@ -0,0 +1,594 @@
from __future__ import annotations
from typing import Literal, Optional
import pyqtgraph as pg
from bec_lib.endpoints import MessageEndpoints
from pydantic import Field, field_validator
from pydantic_core import PydanticCustomError
from qtpy import QtCore, QtGui
from qtpy.QtCore import QSize, Slot
from qtpy.QtWidgets import QSizePolicy, QWidget
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig, EntryValidator
from bec_widgets.widgets.spiral_progress_bar.ring import Ring, RingConfig
class SpiralProgressBarConfig(ConnectionConfig):
color_map: str | None = Field("magma", description="Color scheme for the progress bars.")
min_number_of_bars: int | None = Field(
1, description="Minimum number of progress bars to display."
)
max_number_of_bars: int | None = Field(
10, description="Maximum number of progress bars to display."
)
num_bars: int | None = Field(1, description="Number of progress bars to display.")
gap: int | None = Field(10, description="Gap between progress bars.")
auto_updates: bool | None = Field(
True, description="Enable or disable updates based on scan queue status."
)
rings: list[RingConfig] | None = Field([], description="List of ring configurations.")
@field_validator("num_bars")
def validate_num_bars(cls, v, values):
min_number_of_bars = values.data.get("min_number_of_bars", None)
max_number_of_bars = values.data.get("max_number_of_bars", None)
if min_number_of_bars is not None and max_number_of_bars is not None:
print(
f"Number of bars adjusted to be between defined min:{min_number_of_bars} and max:{max_number_of_bars} number of bars."
)
v = max(min_number_of_bars, min(v, max_number_of_bars))
return v
@field_validator("rings")
def validate_rings(cls, v, values):
if v is not None and v is not []:
num_bars = values.data.get("num_bars", None)
if len(v) != num_bars:
raise PydanticCustomError(
"different number of configs",
f"Length of rings configuration ({len(v)}) does not match the number of bars ({num_bars}).",
{"wrong_value": len(v)},
)
indices = [ring.index for ring in v]
if sorted(indices) != list(range(len(indices))):
raise PydanticCustomError(
"wrong indices",
f"Indices of ring configurations must be unique and in order from 0 to num_bars {num_bars}.",
{"wrong_value": indices},
)
return v
@field_validator("color_map")
def validate_color_map(cls, v, values):
if v is not None and v != "":
if v not in pg.colormap.listMaps():
raise PydanticCustomError(
"unsupported colormap",
f"Colormap '{v}' not found in the current installation of pyqtgraph",
{"wrong_value": v},
)
return v
class SpiralProgressBar(BECConnector, QWidget):
USER_ACCESS = [
"get_all_rpc",
"rpc_id",
"config_dict",
"rings",
"update_config",
"add_ring",
"remove_ring",
"set_precision",
"set_min_max_values",
"set_number_of_bars",
"set_value",
"set_colors_from_map",
"set_colors_directly",
"set_line_widths",
"set_gap",
"set_diameter",
"reset_diameter",
"enable_auto_updates",
]
def __init__(
self,
parent=None,
config: SpiralProgressBarConfig | dict | None = None,
client=None,
gui_id: str | None = None,
num_bars: int | None = None,
):
if config is None:
config = SpiralProgressBarConfig(widget_class=self.__class__.__name__)
self.config = config
else:
if isinstance(config, dict):
config = SpiralProgressBarConfig(**config, widget_class=self.__class__.__name__)
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id)
QWidget.__init__(self, parent=None)
self.get_bec_shortcuts()
self.entry_validator = EntryValidator(self.dev)
self.RID = None
self.values = None
# For updating bar behaviour
self._auto_updates = True
self._rings = []
if num_bars is not None:
self.config.num_bars = max(
self.config.min_number_of_bars, min(num_bars, self.config.max_number_of_bars)
)
self.initialize_bars()
self.enable_auto_updates(self.config.auto_updates)
@property
def rings(self):
return self._rings
@rings.setter
def rings(self, value):
self._rings = value
def update_config(self, config: SpiralProgressBarConfig | dict):
"""
Update the configuration of the widget.
Args:
config(SpiralProgressBarConfig|dict): Configuration to update.
"""
if isinstance(config, dict):
config = SpiralProgressBarConfig(**config, widget_class=self.__class__.__name__)
self.config = config
self.clear_all()
def initialize_bars(self):
"""
Initialize the progress bars.
"""
start_positions = [90 * 16] * self.config.num_bars
directions = [-1] * self.config.num_bars
self.config.rings = [
RingConfig(
widget_class="Ring",
index=i,
start_positions=start_positions[i],
directions=directions[i],
)
for i in range(self.config.num_bars)
]
self._rings = [
Ring(parent_progress_widget=self, config=config) for config in self.config.rings
]
if self.config.color_map:
self.set_colors_from_map(self.config.color_map)
min_size = self._calculate_minimum_size()
self.setMinimumSize(min_size)
self.update()
def add_ring(self, **kwargs) -> Ring:
"""
Add a new progress bar.
Args:
**kwargs: Keyword arguments for the new progress bar.
Returns:
Ring: Ring object.
"""
if self.config.num_bars < self.config.max_number_of_bars:
ring = Ring(parent_progress_widget=self, **kwargs)
ring.config.index = self.config.num_bars
self.config.num_bars += 1
self._rings.append(ring)
self.config.rings.append(ring.config)
if self.config.color_map:
self.set_colors_from_map(self.config.color_map)
self.update()
return ring
def remove_ring(self, index: int):
"""
Remove a progress bar by index.
Args:
index(int): Index of the progress bar to remove.
"""
ring = self._find_ring_by_index(index)
ring.cleanup()
self._rings.remove(ring)
self.config.rings.remove(ring.config)
self.config.num_bars -= 1
self._reindex_rings()
if self.config.color_map:
self.set_colors_from_map(self.config.color_map)
self.update()
def _reindex_rings(self):
"""
Reindex the progress bars.
"""
for i, ring in enumerate(self._rings):
ring.config.index = i
def set_precision(self, precision: int, bar_index: int = None):
"""
Set the precision for the progress bars. If bar_index is not provide, the precision will be set for all progress bars.
Args:
precision(int): Precision for the progress bars.
bar_index(int): Index of the progress bar to set the precision for. If provided, only a single precision can be set.
"""
if bar_index is not None:
bar_index = self._bar_index_check(bar_index)
ring = self._find_ring_by_index(bar_index)
ring.config.precision = precision
else:
for ring in self._rings:
ring.config.precision = precision
self.update()
def set_min_max_values(
self,
min_values: int | float | list[int | float],
max_values: int | float | list[int | float],
):
"""
Set the minimum and maximum values for the progress bars.
Args:
min_values(int|float | list[float]): Minimum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of minimum values for each progress bar.
max_values(int|float | list[float]): Maximum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of maximum values for each progress bar.
"""
if isinstance(min_values, int) or isinstance(min_values, float):
min_values = [min_values]
if isinstance(max_values, int) or isinstance(max_values, float):
max_values = [max_values]
min_values = self._adjust_list_to_bars(min_values)
max_values = self._adjust_list_to_bars(max_values)
for ring, min_value, max_value in zip(self._rings, min_values, max_values):
ring.set_min_max_values(min_value, max_value)
self.update()
def set_number_of_bars(self, num_bars: int):
"""
Set the number of progress bars to display.
Args:
num_bars(int): Number of progress bars to display.
"""
num_bars = max(
self.config.min_number_of_bars, min(num_bars, self.config.max_number_of_bars)
)
if num_bars != self.config.num_bars:
self.config.num_bars = num_bars
self.initialize_bars()
def set_value(self, values: int | list, ring_index: int = None):
"""
Set the values for the progress bars.
Args:
values(int | tuple): Value(s) for the progress bars. If multiple progress bars are displayed, provide a tuple of values for each progress bar.
ring_index(int): Index of the progress bar to set the value for. If provided, only a single value can be set.
Examples:
>>> SpiralProgressBar.set_value(50)
>>> SpiralProgressBar.set_value([30, 40, 50]) # (outer, middle, inner)
>>> SpiralProgressBar.set_value(60, bar_index=1) # Set the value for the middle progress bar.
"""
if ring_index is not None:
ring = self._find_ring_by_index(ring_index)
if isinstance(values, list):
values = values[0]
print(
f"Warning: Only a single value can be set for a single progress bar. Using the first value in the list {values}"
)
ring.set_value(values)
else:
if isinstance(values, int):
values = [values]
values = self._adjust_list_to_bars(values)
for ring, value in zip(self._rings, values):
ring.set_value(value)
self.update()
def set_colors_from_map(self, colormap, color_format: Literal["RGB", "HEX"] = "RGB"):
"""
Set the colors for the progress bars from a colormap.
Args:
colormap(str): Name of the colormap.
color_format(Literal["RGB","HEX"]): Format of the returned colors ('RGB', 'HEX').
"""
if colormap not in pg.colormap.listMaps():
raise ValueError(
f"Colormap '{colormap}' not found in the current installation of pyqtgraph"
)
colors = Colors.golden_angle_color(colormap, self.config.num_bars, color_format)
self.set_colors_directly(colors)
self.config.color_map = colormap
self.update()
def set_colors_directly(self, colors: list[str | tuple] | str | tuple, bar_index: int = None):
"""
Set the colors for the progress bars directly.
Args:
colors(list[str | tuple] | str | tuple): Color(s) for the progress bars. If multiple progress bars are displayed, provide a list of colors for each progress bar.
bar_index(int): Index of the progress bar to set the color for. If provided, only a single color can be set.
"""
if bar_index is not None and isinstance(colors, (str, tuple)):
bar_index = self._bar_index_check(bar_index)
ring = self._find_ring_by_index(bar_index)
ring.set_color(colors)
else:
if isinstance(colors, (str, tuple)):
colors = [colors]
colors = self._adjust_list_to_bars(colors)
for ring, color in zip(self._rings, colors):
ring.set_color(color)
self.config.color_map = None
self.update()
def set_line_widths(self, widths: int | list[int], bar_index: int = None):
"""
Set the line widths for the progress bars.
Args:
widths(int | list[int]): Line width(s) for the progress bars. If multiple progress bars are displayed, provide a list of line widths for each progress bar.
bar_index(int): Index of the progress bar to set the line width for. If provided, only a single line width can be set.
"""
if bar_index is not None:
bar_index = self._bar_index_check(bar_index)
ring = self._find_ring_by_index(bar_index)
if isinstance(widths, list):
widths = widths[0]
print(
f"Warning: Only a single line width can be set for a single progress bar. Using the first value in the list {widths}"
)
ring.set_line_width(widths)
else:
if isinstance(widths, int):
widths = [widths]
widths = self._adjust_list_to_bars(widths)
self.config.gap = max(widths) * 2
for ring, width in zip(self._rings, widths):
ring.set_line_width(width)
min_size = self._calculate_minimum_size()
self.setMinimumSize(min_size)
self.update()
def set_gap(self, gap: int):
"""
Set the gap between the progress bars.
Args:
gap(int): Gap between the progress bars.
"""
self.config.gap = gap
self.update()
def set_diameter(self, diameter: int):
"""
Set the diameter of the widget.
Args:
diameter(int): Diameter of the widget.
"""
size = QSize(diameter, diameter)
self.resize(size)
self.setFixedSize(size)
def _find_ring_by_index(self, index: int) -> Ring:
"""
Find the ring by index.
Args:
index(int): Index of the ring.
Returns:
Ring: Ring object.
"""
found_ring = None
for ring in self._rings:
if ring.config.index == index:
found_ring = ring
break
if found_ring is None:
raise ValueError(f"Ring with index {index} not found.")
return found_ring
def enable_auto_updates(self, enable: bool = True):
"""
Enable or disable updates based on scan status. Overrides manual updates.
The behaviour of the whole progress bar widget will be driven by the scan queue status.
Args:
enable(bool): True or False.
Returns:
bool: True if scan segment updates are enabled.
"""
self._auto_updates = enable
if enable is True:
self.bec_dispatcher.connect_slot(
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
)
else:
self.bec_dispatcher.disconnect_slot(
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
)
return self._auto_updates
@Slot(dict, dict)
def on_scan_queue_status(self, msg, meta):
primary_queue = msg.get("queue").get("primary")
info = primary_queue.get("info", None)
if info:
active_request_block = info[0].get("active_request_block", None)
if active_request_block:
report_instructions = active_request_block.get("report_instructions", None)
if report_instructions:
instruction_type = list(report_instructions[0].keys())[0]
if instruction_type == "scan_progress":
if self.config.num_bars != 1:
self.set_number_of_bars(1)
self._hook_scan_progress(ring_index=0)
elif instruction_type == "readback":
devices = report_instructions[0].get("readback").get("devices")
start = report_instructions[0].get("readback").get("start")
end = report_instructions[0].get("readback").get("end")
if self.config.num_bars != len(devices):
self.set_number_of_bars(len(devices))
for index, device in enumerate(devices):
self._hook_readback(index, device, start[index], end[index])
else:
print(f"{instruction_type} not supported yet.")
# elif instruction_type == "device_progress":
# print("hook device_progress")
def _hook_scan_progress(self, ring_index: int = None):
if ring_index is not None:
ring = self._find_ring_by_index(ring_index)
else:
ring = self._rings[0]
if ring.config.connections.slot == "on_scan_progress":
return
else:
ring.set_connections("on_scan_progress", MessageEndpoints.scan_progress())
def _hook_readback(self, bar_index: int, device: str, min: float | int, max: float | int):
ring = self._find_ring_by_index(bar_index)
ring.set_min_max_values(min, max)
endpoint = MessageEndpoints.device_readback(device)
ring.set_connections("on_device_readback", endpoint)
def _adjust_list_to_bars(self, items: list) -> list:
"""
Utility method to adjust the list of parameters to match the number of progress bars.
Args:
items(list): List of parameters for the progress bars.
Returns:
list: List of parameters for the progress bars.
"""
if items is None:
raise ValueError(
"Items cannot be None. Please provide a list for parameters for the progress bars."
)
if not isinstance(items, list):
items = [items]
if len(items) < self.config.num_bars:
last_item = items[-1]
items.extend([last_item] * (self.config.num_bars - len(items)))
elif len(items) > self.config.num_bars:
items = items[: self.config.num_bars]
return items
def _bar_index_check(self, bar_index: int):
"""
Utility method to check if the bar index is within the range of the number of progress bars.
Args:
bar_index(int): Index of the progress bar to set the value for.
"""
if not (0 <= bar_index < self.config.num_bars):
raise ValueError(
f"bar_index {bar_index} out of range of number of bars {self.config.num_bars}."
)
return bar_index
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
size = min(self.width(), self.height())
rect = QtCore.QRect(0, 0, size, size)
rect.adjust(
max(ring.config.line_width for ring in self._rings),
max(ring.config.line_width for ring in self._rings),
-max(ring.config.line_width for ring in self._rings),
-max(ring.config.line_width for ring in self._rings),
)
for i, ring in enumerate(self._rings):
# Background arc
painter.setPen(
QtGui.QPen(ring.background_color, ring.config.line_width, QtCore.Qt.SolidLine)
)
offset = self.config.gap * i
adjusted_rect = QtCore.QRect(
rect.left() + offset,
rect.top() + offset,
rect.width() - 2 * offset,
rect.height() - 2 * offset,
)
painter.drawArc(adjusted_rect, ring.config.start_position, 360 * 16)
# Foreground arc
pen = QtGui.QPen(ring.color, ring.config.line_width, QtCore.Qt.SolidLine)
pen.setCapStyle(QtCore.Qt.RoundCap)
painter.setPen(pen)
proportion = (ring.value - ring.config.min_value) / (
(ring.config.max_value - ring.config.min_value) + 1e-3
)
angle = int(proportion * 360 * 16 * ring.config.direction)
painter.drawArc(adjusted_rect, ring.start_position, angle)
def reset_diameter(self):
"""
Reset the fixed size of the widget.
"""
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
self.setMinimumSize(self._calculate_minimum_size())
self.setMaximumSize(16777215, 16777215)
def _calculate_minimum_size(self):
"""
Calculate the minimum size of the widget.
"""
if not self.config.rings:
print("no rings to get size from setting size to 10x10")
return QSize(10, 10)
ring_widths = [self.config.rings[i].line_width for i in range(self.config.num_bars)]
total_width = sum(ring_widths) + self.config.gap * (self.config.num_bars - 1)
diameter = total_width * 2
if diameter < 50:
diameter = 50
return QSize(diameter, diameter)
def sizeHint(self):
min_size = self._calculate_minimum_size()
return min_size
def clear_all(self):
for ring in self._rings:
ring.cleanup()
del ring
self._rings = []
self.update()
self.initialize_bars()
def cleanup(self):
self.bec_dispatcher.disconnect_slot(
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
)
for ring in self._rings:
ring.cleanup()
del ring
super().cleanup()

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "0.54.0"
version = "0.55.0"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [
@@ -38,6 +38,7 @@ dev = [
]
pyqt5 = ["PyQt5>=5.9"]
pyqt6 = ["PyQt6>=6.7"]
pyside6 = ["PySide6>=6.7"]
[project.urls]
"Bug Tracker" = "https://gitlab.psi.ch/bec/bec_widgets/issues"

View File

@@ -3,6 +3,7 @@ import pytest
from bec_lib.endpoints import MessageEndpoints
from bec_widgets.cli.client import BECDockArea, BECFigure, BECImageShow, BECMotorMap, BECWaveform
from bec_widgets.utils import Colors
def test_rpc_add_dock_with_figure_e2e(rpc_server_dock, qtbot):
@@ -143,3 +144,83 @@ def test_dock_manipulations_e2e(rpc_server_dock, qtbot):
assert len(dock_server.docks) == 0
assert len(dock_server.tempAreas) == 0
def test_spiral_bar(rpc_server_dock):
dock = BECDockArea(rpc_server_dock.gui_id)
dock_server = rpc_server_dock.gui
d0 = dock.add_dock("dock_0")
bar = d0.add_widget_bec("SpiralProgressBar")
assert bar.__class__.__name__ == "SpiralProgressBar"
bar.set_number_of_bars(5)
bar.set_colors_from_map("viridis")
bar.set_value([10, 20, 30, 40, 50])
bar_server = dock_server.docks["dock_0"].widgets[0]
expected_colors = Colors.golden_angle_color("viridis", 5, "RGB")
bar_colors = [ring.color.getRgb() for ring in bar_server.rings]
bar_values = [ring.value for ring in bar_server.rings]
assert bar_values == [10, 20, 30, 40, 50]
assert bar_colors == expected_colors
def test_spiral_bar_scan_update(rpc_server_dock, qtbot):
dock = BECDockArea(rpc_server_dock.gui_id)
dock_server = rpc_server_dock.gui
d0 = dock.add_dock("dock_0")
d0.add_widget_bec("SpiralProgressBar")
client = rpc_server_dock.client
dev = client.device_manager.devices
scans = client.scans
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
while not status.status == "COMPLETED":
qtbot.wait(200)
qtbot.wait(200)
bar_server = dock_server.docks["dock_0"].widgets[0]
assert bar_server.config.num_bars == 1
np.testing.assert_allclose(bar_server.rings[0].value, 10, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.min_value, 0, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.max_value, 10, atol=0.1)
status = scans.grid_scan(dev.samx, -5, 5, 4, dev.samy, -10, 10, 4, relative=True, exp_time=0.1)
while not status.status == "COMPLETED":
qtbot.wait(200)
qtbot.wait(200)
assert bar_server.config.num_bars == 1
np.testing.assert_allclose(bar_server.rings[0].value, 16, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.min_value, 0, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.max_value, 16, atol=0.1)
init_samx = dev.samx.read()["samx"]["value"]
init_samy = dev.samy.read()["samy"]["value"]
final_samx = init_samx + 5
final_samy = init_samy + 10
dev.samx.velocity.put(5)
dev.samy.velocity.put(5)
status = scans.umv(dev.samx, 5, dev.samy, 10, relative=True)
while not status.status == "COMPLETED":
qtbot.wait(200)
qtbot.wait(200)
assert bar_server.config.num_bars == 2
np.testing.assert_allclose(bar_server.rings[0].value, final_samx, atol=0.1)
np.testing.assert_allclose(bar_server.rings[1].value, final_samy, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.min_value, init_samx, atol=0.1)
np.testing.assert_allclose(bar_server.rings[1].config.min_value, init_samy, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.max_value, final_samx, atol=0.1)
np.testing.assert_allclose(bar_server.rings[1].config.max_value, final_samy, atol=0.1)

View File

@@ -420,11 +420,11 @@ def test_delete_selected_row(motor_coordinate_table):
motor_coordinate_table.add_coordinate((3.0, 4.0))
# Select the row
motor_coordinate_table.table.selectRow(0)
motor_coordinate_table.ui.table.selectRow(0)
# Delete the selected row
motor_coordinate_table.delete_selected_row()
assert motor_coordinate_table.table.rowCount() == 1
assert motor_coordinate_table.ui.table.rowCount() == 1
def test_add_coordinate_and_table_update(motor_coordinate_table):
@@ -433,20 +433,24 @@ def test_add_coordinate_and_table_update(motor_coordinate_table):
# Add coordinate in Individual mode
motor_coordinate_table.add_coordinate((1.0, 2.0))
assert motor_coordinate_table.table.rowCount() == 1
assert motor_coordinate_table.ui.table.rowCount() == 1
# Check if the coordinates match
x_item_individual = motor_coordinate_table.table.cellWidget(0, 3) # Assuming X is in column 3
y_item_individual = motor_coordinate_table.table.cellWidget(0, 4) # Assuming Y is in column 4
x_item_individual = motor_coordinate_table.ui.table.cellWidget(
0, 3
) # Assuming X is in column 3
y_item_individual = motor_coordinate_table.ui.table.cellWidget(
0, 4
) # Assuming Y is in column 4
assert float(x_item_individual.text()) == 1.0
assert float(y_item_individual.text()) == 2.0
# Switch to Start/Stop and add coordinates
motor_coordinate_table.comboBox_mode.setCurrentIndex(1) # Switch mode
motor_coordinate_table.ui.comboBox_mode.setCurrentIndex(1) # Switch mode
motor_coordinate_table.add_coordinate((3.0, 4.0))
motor_coordinate_table.add_coordinate((5.0, 6.0))
assert motor_coordinate_table.table.rowCount() == 1
assert motor_coordinate_table.ui.table.rowCount() == 1
def test_plot_coordinates_signal(motor_coordinate_table):
@@ -466,26 +470,26 @@ def test_plot_coordinates_signal(motor_coordinate_table):
assert received
def test_move_motor_action(motor_coordinate_table):
# Add a coordinate
motor_coordinate_table.add_coordinate((1.0, 2.0))
# Mock the motor thread move_absolute function
motor_coordinate_table.motor_thread.move_absolute = MagicMock()
# Trigger the move action
move_button = motor_coordinate_table.table.cellWidget(0, 1)
move_button.click()
motor_coordinate_table.motor_thread.move_absolute.assert_called_with(
motor_coordinate_table.motor_x, motor_coordinate_table.motor_y, (1.0, 2.0)
)
# def test_move_motor_action(motor_coordinate_table,qtbot):#TODO enable again after table refactor
# # Add a coordinate
# motor_coordinate_table.add_coordinate((1.0, 2.0))
#
# # Mock the motor thread move_absolute function
# motor_coordinate_table.motor_thread.move_absolute = MagicMock()
#
# # Trigger the move action
# move_button = motor_coordinate_table.table.cellWidget(0, 1)
# move_button.click()
#
# motor_coordinate_table.motor_thread.move_absolute.assert_called_with(
# motor_coordinate_table.motor_x, motor_coordinate_table.motor_y, (1.0, 2.0)
# )
def test_plot_coordinates_signal_individual(motor_coordinate_table, qtbot):
motor_coordinate_table.warning_message = False
motor_coordinate_table.set_precision(3)
motor_coordinate_table.comboBox_mode.setCurrentIndex(0)
motor_coordinate_table.ui.comboBox_mode.setCurrentIndex(0)
# This list will store the signals emitted during the test
emitted_signals = []
@@ -506,8 +510,8 @@ def test_plot_coordinates_signal_individual(motor_coordinate_table, qtbot):
assert len(coordinates) > 0, "Coordinates list is empty."
assert reference_tag == "Individual"
assert color == "green"
assert motor_coordinate_table.table.cellWidget(0, 3).text() == "1.000"
assert motor_coordinate_table.table.cellWidget(0, 4).text() == "2.000"
assert motor_coordinate_table.ui.table.cellWidget(0, 3).text() == "1.000"
assert motor_coordinate_table.ui.table.cellWidget(0, 4).text() == "2.000"
#######################################################

View File

@@ -0,0 +1,338 @@
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
import pytest
from bec_lib.endpoints import MessageEndpoints
from pydantic import ValidationError
from bec_widgets.utils import Colors
from bec_widgets.widgets import SpiralProgressBar
from bec_widgets.widgets.spiral_progress_bar.ring import RingConfig, RingConnections
from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBarConfig
from .client_mocks import mocked_client
@pytest.fixture
def spiral_progress_bar(qtbot, mocked_client):
widget = SpiralProgressBar(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_bar_init(spiral_progress_bar):
assert spiral_progress_bar is not None
assert spiral_progress_bar.client is not None
assert isinstance(spiral_progress_bar, SpiralProgressBar)
assert spiral_progress_bar.config.widget_class == "SpiralProgressBar"
assert spiral_progress_bar.config.gui_id is not None
assert spiral_progress_bar.gui_id == spiral_progress_bar.config.gui_id
def test_config_validation_num_of_bars():
config = SpiralProgressBarConfig(num_bars=100, min_num_bars=1, max_num_bars=10)
assert config.num_bars == 10
def test_config_validation_num_of_ring_error():
ring_config_0 = RingConfig(index=0)
ring_config_1 = RingConfig(index=1)
with pytest.raises(ValidationError) as excinfo:
SpiralProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=1)
errors = excinfo.value.errors()
assert len(errors) == 1
assert errors[0]["type"] == "different number of configs"
assert "Length of rings configuration (2) does not match the number of bars (1)." in str(
excinfo.value
)
def test_config_validation_ring_indices_wrong_order():
ring_config_0 = RingConfig(index=2)
ring_config_1 = RingConfig(index=5)
with pytest.raises(ValidationError) as excinfo:
SpiralProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=2)
errors = excinfo.value.errors()
assert len(errors) == 1
assert errors[0]["type"] == "wrong indices"
assert (
"Indices of ring configurations must be unique and in order from 0 to num_bars 2."
in str(excinfo.value)
)
def test_config_validation_ring_same_indices():
ring_config_0 = RingConfig(index=0)
ring_config_1 = RingConfig(index=0)
with pytest.raises(ValidationError) as excinfo:
SpiralProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=2)
errors = excinfo.value.errors()
assert len(errors) == 1
assert errors[0]["type"] == "wrong indices"
assert (
"Indices of ring configurations must be unique and in order from 0 to num_bars 2."
in str(excinfo.value)
)
def test_config_validation_invalid_colormap():
with pytest.raises(ValueError) as excinfo:
SpiralProgressBarConfig(color_map="crazy_colors")
errors = excinfo.value.errors()
assert len(errors) == 1
assert errors[0]["type"] == "unsupported colormap"
assert "Colormap 'crazy_colors' not found in the current installation of pyqtgraph" in str(
excinfo.value
)
def test_ring_connection_endpoint_validation():
with pytest.raises(ValueError) as excinfo:
RingConnections(slot="on_scan_progress", endpoint="non_existing")
errors = excinfo.value.errors()
assert len(errors) == 1
assert errors[0]["type"] == "unsupported endpoint"
assert (
"For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'."
in str(excinfo.value)
)
with pytest.raises(ValueError) as excinfo:
RingConnections(slot="on_device_readback", endpoint="non_existing")
errors = excinfo.value.errors()
assert len(errors) == 1
assert errors[0]["type"] == "unsupported endpoint"
assert (
"For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'."
in str(excinfo.value)
)
def test_bar_add_number_of_bars(spiral_progress_bar):
assert spiral_progress_bar.config.num_bars == 1
spiral_progress_bar.set_number_of_bars(5)
assert spiral_progress_bar.config.num_bars == 5
spiral_progress_bar.set_number_of_bars(2)
assert spiral_progress_bar.config.num_bars == 2
def test_add_remove_bars_individually(spiral_progress_bar):
spiral_progress_bar.add_ring()
spiral_progress_bar.add_ring()
assert spiral_progress_bar.config.num_bars == 3
assert len(spiral_progress_bar.config.rings) == 3
spiral_progress_bar.remove_ring(1)
assert spiral_progress_bar.config.num_bars == 2
assert len(spiral_progress_bar.config.rings) == 2
assert spiral_progress_bar.rings[0].config.index == 0
assert spiral_progress_bar.rings[1].config.index == 1
def test_bar_set_value(spiral_progress_bar):
spiral_progress_bar.set_number_of_bars(5)
assert spiral_progress_bar.config.num_bars == 5
assert len(spiral_progress_bar.config.rings) == 5
assert len(spiral_progress_bar.rings) == 5
spiral_progress_bar.set_value([10, 20, 30, 40, 50])
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [10, 20, 30, 40, 50]
# update just one bar
spiral_progress_bar.set_value(90, 1)
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [10, 90, 30, 40, 50]
def test_bar_set_precision(spiral_progress_bar):
spiral_progress_bar.set_number_of_bars(3)
assert spiral_progress_bar.config.num_bars == 3
assert len(spiral_progress_bar.config.rings) == 3
assert len(spiral_progress_bar.rings) == 3
spiral_progress_bar.set_precision(2)
ring_precision = [ring.config.precision for ring in spiral_progress_bar.rings]
assert ring_precision == [2, 2, 2]
spiral_progress_bar.set_value([10.1234, 20.1234, 30.1234])
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [10.12, 20.12, 30.12]
spiral_progress_bar.set_precision(4, 1)
ring_precision = [ring.config.precision for ring in spiral_progress_bar.rings]
assert ring_precision == [2, 4, 2]
spiral_progress_bar.set_value([10.1234, 20.1234, 30.1234])
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [10.12, 20.1234, 30.12]
def test_set_min_max_value(spiral_progress_bar):
spiral_progress_bar.set_number_of_bars(2)
spiral_progress_bar.set_min_max_values(0, 10)
ring_min_values = [ring.config.min_value for ring in spiral_progress_bar.rings]
ring_max_values = [ring.config.max_value for ring in spiral_progress_bar.rings]
assert ring_min_values == [0, 0]
assert ring_max_values == [10, 10]
spiral_progress_bar.set_value([5, 15])
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [5, 10]
def test_setup_colors_from_colormap(spiral_progress_bar):
spiral_progress_bar.set_number_of_bars(5)
spiral_progress_bar.set_colors_from_map("viridis", "RGB")
expected_colors = Colors.golden_angle_color("viridis", 5, "RGB")
converted_colors = [ring.color.getRgb() for ring in spiral_progress_bar.rings]
ring_config_colors = [ring.config.color for ring in spiral_progress_bar.rings]
assert expected_colors == converted_colors
assert ring_config_colors == expected_colors
def get_colors_from_rings(rings):
converted_colors = [ring.color.getRgb() for ring in rings]
ring_config_colors = [ring.config.color for ring in rings]
return converted_colors, ring_config_colors
def test_set_colors_from_colormap_and_change_num_of_bars(spiral_progress_bar):
spiral_progress_bar.set_number_of_bars(2)
spiral_progress_bar.set_colors_from_map("viridis", "RGB")
expected_colors = Colors.golden_angle_color("viridis", 2, "RGB")
converted_colors, ring_config_colors = get_colors_from_rings(spiral_progress_bar.rings)
assert expected_colors == converted_colors
assert ring_config_colors == expected_colors
# increase the number of bars to 6
spiral_progress_bar.set_number_of_bars(6)
expected_colors = Colors.golden_angle_color("viridis", 6, "RGB")
converted_colors, ring_config_colors = get_colors_from_rings(spiral_progress_bar.rings)
assert expected_colors == converted_colors
assert ring_config_colors == expected_colors
# decrease the number of bars to 3
spiral_progress_bar.set_number_of_bars(3)
expected_colors = Colors.golden_angle_color("viridis", 3, "RGB")
converted_colors, ring_config_colors = get_colors_from_rings(spiral_progress_bar.rings)
assert expected_colors == converted_colors
assert ring_config_colors == expected_colors
def test_set_colors_directly(spiral_progress_bar):
spiral_progress_bar.set_number_of_bars(3)
# setting as a list of rgb tuples
colors = [(255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255)]
spiral_progress_bar.set_colors_directly(colors)
converted_colors = get_colors_from_rings(spiral_progress_bar.rings)[0]
assert colors == converted_colors
spiral_progress_bar.set_colors_directly((255, 0, 0, 255), 1)
converted_colors = get_colors_from_rings(spiral_progress_bar.rings)[0]
assert converted_colors == [(255, 0, 0, 255), (255, 0, 0, 255), (0, 0, 255, 255)]
def test_set_line_width(spiral_progress_bar):
spiral_progress_bar.set_number_of_bars(3)
spiral_progress_bar.set_line_widths(5)
line_widths = [ring.config.line_width for ring in spiral_progress_bar.rings]
assert line_widths == [5, 5, 5]
spiral_progress_bar.set_line_widths([10, 20, 30])
line_widths = [ring.config.line_width for ring in spiral_progress_bar.rings]
assert line_widths == [10, 20, 30]
spiral_progress_bar.set_line_widths(15, 1)
line_widths = [ring.config.line_width for ring in spiral_progress_bar.rings]
assert line_widths == [10, 15, 30]
def test_set_gap(spiral_progress_bar):
spiral_progress_bar.set_number_of_bars(3)
spiral_progress_bar.set_gap(20)
assert spiral_progress_bar.config.gap == 20
def test_auto_update(spiral_progress_bar):
spiral_progress_bar.enable_auto_updates(True)
scan_queue_status_scan_progress = {
"queue": {
"primary": {
"info": [{"active_request_block": {"report_instructions": [{"scan_progress": 10}]}}]
}
}
}
meta = {}
spiral_progress_bar.on_scan_queue_status(scan_queue_status_scan_progress, meta)
assert spiral_progress_bar._auto_updates is True
assert len(spiral_progress_bar._rings) == 1
assert spiral_progress_bar._rings[0].config.connections == RingConnections(
slot="on_scan_progress", endpoint=MessageEndpoints.scan_progress()
)
scan_queue_status_device_readback = {
"queue": {
"primary": {
"info": [
{
"active_request_block": {
"report_instructions": [
{
"readback": {
"devices": ["samx", "samy"],
"start": [1, 2],
"end": [10, 20],
}
}
]
}
}
]
}
}
}
spiral_progress_bar.on_scan_queue_status(scan_queue_status_device_readback, meta)
assert spiral_progress_bar._auto_updates is True
assert len(spiral_progress_bar._rings) == 2
assert spiral_progress_bar._rings[0].config.connections == RingConnections(
slot="on_device_readback", endpoint=MessageEndpoints.device_readback("samx")
)
assert spiral_progress_bar._rings[1].config.connections == RingConnections(
slot="on_device_readback", endpoint=MessageEndpoints.device_readback("samy")
)
assert spiral_progress_bar._rings[0].config.min_value == 1
assert spiral_progress_bar._rings[0].config.max_value == 10
assert spiral_progress_bar._rings[1].config.min_value == 2
assert spiral_progress_bar._rings[1].config.max_value == 20