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

Compare commits

...

27 Commits

Author SHA1 Message Date
8d363e58d6 wip 2024-07-09 18:02:38 +02:00
a9b808d86a fix(terminal): added default args to avoid designer crashes on startup 2024-07-08 19:38:30 +02:00
7018506872 test(vscode): fixed vscode tests for new cleanup routine 2024-07-08 19:38:05 +02:00
b5e2c83492 fix(widget): fixed widget cleanup routine 2024-07-08 17:18:40 +02:00
08a39bf41c fix(bec_widget): added cleanup method to bec widget base class 2024-07-08 16:17:46 +02:00
b9f2abcb21 test(vscode): improved vscode test 2024-07-08 09:08:00 +02:00
88996e8cde feat: added reference utils to compare renderings of widgets 2024-07-08 09:08:00 +02:00
b1d5fbbfad feat(widgets): added device box with spinner 2024-07-08 09:08:00 +02:00
be0915c054 fix(website): fixed dummy input 2024-07-08 09:08:00 +02:00
767a492613 feat(designer): added option to skip the widget validation for DesignerPluginGenerator 2024-07-08 09:08:00 +02:00
semantic-release
b9f9a003a2 0.82.1
Automatically generated by python-semantic-release
2024-07-07 21:12:19 +00:00
734f4c7750 tests(motor_map_widget): tests added 2024-07-07 22:59:02 +02:00
c78cd898f2 fix(motor_map): bug where motors without limits were selected 2024-07-07 22:59:02 +02:00
74a249bd06 test(setting_dialog): tests added 2024-07-07 22:59:02 +02:00
2020953b93 feat(settings_dialog):apply button 2024-07-07 22:59:02 +02:00
3826bb3d9e refactor(setting_dialog): moved to qt_utils 2024-07-07 22:59:02 +02:00
7ffc06f3c7 refactor(toolbar): toolbar moved from widgets to qt_utils 2024-07-07 22:59:02 +02:00
semantic-release
eea1a75d4a 0.82.0
Automatically generated by python-semantic-release
2024-07-07 20:58:30 +00:00
b9bff38b64 feat(toggle): added angular component-like toggle 2024-07-07 22:50:25 +02:00
f04862933f refactor(device_input): DeviceComboBox and DeviceLineEdit moved to top layer of widgets 2024-07-07 18:53:32 +02:00
f5b8375fd3 refactor(stop_button): moved to top layer, plugin added 2024-07-07 18:53:32 +02:00
db1cdf4280 refactor(motor_map_widget): removed restriction of only PySide6 for widget 2024-07-07 18:42:51 +02:00
fa1e86ff07 refactor(color_button): ColorButton moved to top level of widgets 2024-07-07 18:42:51 +02:00
semantic-release
9a95454723 0.81.2
Automatically generated by python-semantic-release
2024-07-07 16:28:57 +00:00
dd1875ea5c fix(waveform): scan_history error check for IndexError 2024-07-07 18:21:08 +02:00
semantic-release
df4fabb32a 0.81.1
Automatically generated by python-semantic-release
2024-07-07 11:45:09 +00:00
99114f14f6 fix(motor_control): temporary remove of motor control widgets 2024-07-07 11:41:07 +02:00
107 changed files with 2097 additions and 3910 deletions

View File

@@ -144,6 +144,9 @@ tests:
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- tests/reference_failures/
when: always
test-matrix:
parallel:

View File

@@ -1,5 +1,55 @@
# CHANGELOG
## v0.82.1 (2024-07-07)
### Fix
* fix(motor_map): bug where motors without limits were selected ([`c78cd89`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c78cd898f203f950d7cb589eb5609feaa88062cf))
### Refactor
* refactor(setting_dialog): moved to qt_utils ([`3826bb3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3826bb3d9e870e85709b5b20ef09a4d22641280c))
* refactor(toolbar): toolbar moved from widgets to qt_utils ([`7ffc06f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7ffc06f3c7ddd86a1681408a75221b9bbadb236b))
### Test
* test(setting_dialog): tests added ([`74a249b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/74a249bd065d01006cb532bfff2a9bfedb34b592))
### Unknown
* tests(motor_map_widget): tests added ([`734f4c7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/734f4c77507a1edafd477d81b5f7401d8e759be2))
* feat(settings_dialog):apply button ([`2020953`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2020953b933b6fcad61ecc770588d39518c26fdd))
## v0.82.0 (2024-07-07)
### Feature
* feat(toggle): added angular component-like toggle ([`b9bff38`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b9bff38b64b86f06b3bc047922ef9df0c7d32e71))
### Refactor
* refactor(device_input): DeviceComboBox and DeviceLineEdit moved to top layer of widgets ([`f048629`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f04862933f049030554086adef3ec9e1aebd3eda))
* refactor(stop_button): moved to top layer, plugin added ([`f5b8375`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f5b8375fd36e3bb681de571da86a6c0bdb3cb6f0))
* refactor(motor_map_widget): removed restriction of only PySide6 for widget ([`db1cdf4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/db1cdf42806fef6d7c6d2db83528f32df3f9751d))
* refactor(color_button): ColorButton moved to top level of widgets ([`fa1e86f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fa1e86ff07b25d2c47c73117b00765b8e2f25da4))
## v0.81.2 (2024-07-07)
### Fix
* fix(waveform): scan_history error check for IndexError ([`dd1875e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dd1875ea5cc18bcef9aad743347a8accf144c08d))
## v0.81.1 (2024-07-07)
### Fix
* fix(motor_control): temporary remove of motor control widgets ([`99114f1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99114f14f62202e1fd8bf145616fa8c69937ada4))
## v0.81.0 (2024-07-06)
### Feature
@@ -86,8 +136,6 @@
* feat(bec_connector): export config to yaml ([`a391f30`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a391f3018c50fee6a4a06884491b957df80c3cd3))
* feat(utils): colors added convertor for rgba to hex ([`572f2fb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/572f2fb8110d5cb0e80f3ca45ce57ef405572456))
### Fix
* fix(waveform): scatter 2D brush error ([`215d59c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/215d59c8bfe7fda9aff8cec8353bef9e1ce2eca1))
@@ -97,47 +145,3 @@
* fix(figure): if/else logic corrected in subplot_factory ([`3e78723`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3e787234c7274b0698423d7bf9a4c54ec46bad5f))
* fix(image): processing of already displayed data; closes #106 ([`1173510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1173510105d2d70d7e498c2ac1e122cea3a16597))
* fix(bec_figure): full reconstruction with config from other bec figure ([`b6e1e20`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b6e1e20b7c8549bb092e981062329e601411dda6))
* fix(motor_map): API changes updates current visualisation; motor_map can be initialised from config ([`2e2d422`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2e2d422910685a2527a3d961a468c787f771ca44))
* fix(image): image add_custom_image fixed, closes #225 ([`f0556e4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0556e44113ffee66cf735aa2dd758c62cb634f4))
* fix(figure): subplot methods consolidated; added subplot factory ([`4a97105`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4a97105e4bd2ce77d72dfe5f8307dd9ee65b21b0))
* fix(image): image can be fully reconstructed from config ([`797f73c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/797f73c39aa73e07d6311f3de4baea53f6c380e0))
* fix(image_item): vrange added int for pydantic model check ([`b8f796f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b8f796fd3fcc15641e8fc6a3ca75c344ce90fc45))
* fix(bec_figure): waveforms can be initialised from the config; widgets are deleteLater after removal ([`78673ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/78673ea11a47aad878128197ae6213925228ed59))
### Unknown
* Resolve "add VT100 console executing BEC as a widget" ([`c6a14c0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c6a14c0768a90695567a83a7895247ed0c64f3ce))
## v0.76.1 (2024-06-29)
### Fix
* fix(plugins): fixes and tests for auto-gen plugins ([`c42511d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c42511dd44cc13577e108a6cef3166376e594f54))
## v0.76.0 (2024-06-28)
### Feature
* feat(designer): added support for creating designer plugins automatically ([`c1dd0ee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c1dd0ee1906dba1f2e2ae9ce40a84d55c26a1cce))
### Fix
* fix: fixed qwidget inheritance for ring progress bar ([`0610d2f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0610d2f9f027f8659e7149f2dfbb316ff30e337d))
### Unknown
* fix:parent set as first kwarg TextBox and WebsiteWidget ([`a45c407`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a45c4075684b93bfdcee03e5a416b84f61d3bc6f))
## v0.75.0 (2024-06-26)
### Feature
* feat(widgets): added simple bec queue widget ([`3faee98`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3faee98ec80041a27e4c1f1156178de6f9dcdc63))

View File

@@ -21,6 +21,7 @@ class Widgets(str, enum.Enum):
BECStatusBox = "BECStatusBox"
RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl"
StopButton = "StopButton"
TextBox = "TextBox"
VSCodeEditor = "VSCodeEditor"
WebsiteWidget = "WebsiteWidget"

View File

@@ -1,9 +0,0 @@
from .motor_movement import (
MotorControlApp,
MotorControlMap,
MotorControlPanel,
MotorControlPanelAbsolute,
MotorControlPanelRelative,
MotorCoordinateTable,
MotorThread,
)

View File

@@ -1,9 +0,0 @@
from .motor_control_compilations import (
MotorControlApp,
MotorControlMap,
MotorControlPanel,
MotorControlPanelAbsolute,
MotorControlPanelRelative,
MotorCoordinateTable,
MotorThread,
)

View File

@@ -1,250 +0,0 @@
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
import qdarktheme
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication, QSplitter, QVBoxLayout, QWidget
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets.motor_control.motor_control import MotorThread
from bec_widgets.widgets.motor_control.motor_table.motor_table import MotorCoordinateTable
from bec_widgets.widgets.motor_control.movement_absolute.movement_absolute import (
MotorControlAbsolute,
)
from bec_widgets.widgets.motor_control.movement_relative.movement_relative import (
MotorControlRelative,
)
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": 3,
"precision": 4,
"step_x_y_same": False,
"move_with_arrows": False,
},
"plot_settings": {
"colormap": "Greys",
"scatter_size": 5,
"max_points": 1000,
"num_dim_points": 100,
"precision": 2,
"num_columns": 1,
"background_value": 25,
},
"motors": [
{
"plot_name": "Motor Map",
"x_label": "Motor X",
"y_label": "Motor Y",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "samy", "entry": "samy"}],
},
}
],
}
class MotorControlApp(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
# Widgets
self.motor_control_panel = MotorControlPanel(client=self.client, config=self.config)
# Create MotorMap
# self.motion_map = MotorMap(client=self.client, config=self.config)
# Create MotorCoordinateTable
self.motor_table = MotorCoordinateTable(client=self.client, config=self.config)
# Create the splitter and add MotorMap and MotorControlPanel
splitter = QSplitter(Qt.Horizontal)
# splitter.addWidget(self.motion_map)
splitter.addWidget(self.motor_control_panel)
splitter.addWidget(self.motor_table)
# Set the main layout
layout = QVBoxLayout(self)
layout.addWidget(splitter)
self.setLayout(layout)
# Connecting signals and slots
# self.motor_control_panel.selection_widget.selected_motors_signal.connect(
# lambda x, y: self.motion_map.change_motors(x, y, 0)
# )
self.motor_control_panel.absolute_widget.coordinates_signal.connect(
self.motor_table.add_coordinate
)
self.motor_control_panel.relative_widget.precision_signal.connect(
self.motor_table.set_precision
)
self.motor_control_panel.relative_widget.precision_signal.connect(
self.motor_control_panel.absolute_widget.set_precision
)
# self.motor_table.plot_coordinates_signal.connect(self.motion_map.plot_saved_coordinates)
class MotorControlMap(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
# Widgets
self.motor_control_panel = MotorControlPanel(client=self.client, config=self.config)
# Create MotorMap
# self.motion_map = MotorMap(client=self.client, config=self.config)
# Create the splitter and add MotorMap and MotorControlPanel
splitter = QSplitter(Qt.Horizontal)
# splitter.addWidget(self.motion_map)
splitter.addWidget(self.motor_control_panel)
# Set the main layout
layout = QVBoxLayout(self)
layout.addWidget(splitter)
self.setLayout(layout)
# Connecting signals and slots
# self.motor_control_panel.selection_widget.selected_motors_signal.connect(
# lambda x, y: self.motion_map.change_motors(x, y, 0)
# )
class MotorControlPanel(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
self.motor_thread = MotorThread(client=self.client)
self.selection_widget = MotorControlSelection(
client=self.client, config=self.config, motor_thread=self.motor_thread
)
self.relative_widget = MotorControlRelative(
client=self.client, config=self.config, motor_thread=self.motor_thread
)
self.absolute_widget = MotorControlAbsolute(
client=self.client, config=self.config, motor_thread=self.motor_thread
)
layout = QVBoxLayout(self)
layout.addWidget(self.selection_widget)
layout.addWidget(self.relative_widget)
layout.addWidget(self.absolute_widget)
# Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
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 MotorControlPanelAbsolute(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
self.motor_thread = MotorThread(client=self.client)
self.selection_widget = MotorControlSelection(
client=client, config=config, motor_thread=self.motor_thread
)
self.absolute_widget = MotorControlAbsolute(
client=client, config=config, motor_thread=self.motor_thread
)
layout = QVBoxLayout(self)
layout.addWidget(self.selection_widget)
layout.addWidget(self.absolute_widget)
# Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
class MotorControlPanelRelative(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
self.motor_thread = MotorThread(client=self.client)
self.selection_widget = MotorControlSelection(
client=client, config=config, motor_thread=self.motor_thread
)
self.relative_widget = MotorControlRelative(
client=client, config=config, motor_thread=self.motor_thread
)
layout = QVBoxLayout(self)
layout.addWidget(self.selection_widget)
layout.addWidget(self.relative_widget)
# Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
if __name__ == "__main__": # pragma: no cover
import argparse
import sys
parser = argparse.ArgumentParser(description="Run various Motor Control Widgets compositions.")
parser.add_argument(
"-v",
"--variant",
type=str,
choices=["app", "map", "panel", "panel_abs", "panel_rel"],
help="Select the variant of the motor control to run. "
"'app' for the full application, "
"'map' for MotorMap, "
"'panel' for the MotorControlPanel, "
"'panel_abs' for MotorControlPanel with absolute control, "
"'panel_rel' for MotorControlPanel with relative control.",
)
args = parser.parse_args()
bec_dispatcher = BECDispatcher()
client = bec_dispatcher.client
client.start()
app = QApplication([])
qdarktheme.setup_theme("auto")
if args.variant == "app":
window = MotorControlApp(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "map":
window = MotorControlMap(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel":
window = MotorControlPanel(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel_abs":
window = MotorControlPanelAbsolute(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel_rel":
window = MotorControlPanelRelative(client=client) # , config=CONFIG_DEFAULT)
else:
print("Please specify a valid variant to run. Use -h for help.")
print("Running the full application by default.")
window = MotorControlApp(client=client) # , config=CONFIG_DEFAULT)
window.show()
sys.exit(app.exec())

View File

@@ -1,926 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1561</width>
<height>748</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1409</width>
<height>748</height>
</size>
</property>
<property name="windowTitle">
<string>Motor Controller</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="8,5,8">
<item>
<widget class="GraphicsLayoutWidget" name="glw">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="Controls">
<property name="minimumSize">
<size>
<width>221</width>
<height>471</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6" stretch="1,1,1,0,1">
<property name="spacing">
<number>1</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<widget class="QGroupBox" name="motorSelection">
<property name="minimumSize">
<size>
<width>261</width>
<height>145</height>
</size>
</property>
<property name="title">
<string>Motor Selection</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Motor Y</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_motor_x"/>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_motor_y"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Motor X</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QPushButton" name="pushButton_connecMotors">
<property name="text">
<string>Connect Motors</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>18</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="motorControl">
<property name="minimumSize">
<size>
<width>261</width>
<height>339</height>
</size>
</property>
<property name="title">
<string>Motor Relative</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="checkBox_enableArrows">
<property name="text">
<string>Move with arrow keys</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_same_xy">
<property name="text">
<string>Step [X] = Step [Y]</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="step_grid">
<item row="2" column="0">
<widget class="QLabel" name="label_step_y">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [Y]</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Decimal</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_x">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_step_x">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [X]</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_y">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_precision">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>8</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="direction_grid">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item row="1" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_up">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::UpArrow</enum>
</property>
</widget>
</item>
<item row="2" column="4">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_down">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::DownArrow</enum>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="toolButton_left">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::LeftArrow</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="toolButton_right">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::RightArrow</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>18</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="motorControl_absolute">
<property name="minimumSize">
<size>
<width>261</width>
<height>195</height>
</size>
</property>
<property name="title">
<string>Move Absolute</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox_save_with_go">
<property name="text">
<string>Save position with Go</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_absolute_y">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QDoubleSpinBox" name="spinBox_absolute_x">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="pushButton_save">
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_set">
<property name="text">
<string>Set</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_go_absolute">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="pushButton_stop">
<property name="text">
<string>Stop Movement</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget_tables">
<property name="enabled">
<bool>true</bool>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_coordinates">
<attribute name="title">
<string>Coordinates</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Entries Mode:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<item>
<property name="text">
<string>Individual</string>
</property>
</item>
<item>
<property name="text">
<string>Start/Stop</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="tableWidget_coordinates">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Show</string>
</property>
</column>
<column>
<property name="text">
<string>Move</string>
</property>
</column>
<column>
<property name="text">
<string>Tag</string>
</property>
</column>
<column>
<property name="text">
<string>X</string>
</property>
</column>
<column>
<property name="text">
<string>Y</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_resize_table">
<property name="text">
<string>Resize Table</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkBox_resize_auto">
<property name="text">
<string>Resize Auto</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="pushButton_importCSV">
<property name="text">
<string>Import CSV</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="pushButton_exportCSV">
<property name="text">
<string>Export CSV</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="pushButton_help">
<property name="text">
<string>Help</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_duplicate">
<property name="text">
<string>Duplicate Last Entry</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_settings">
<attribute name="title">
<string>Settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="motorLimits">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Motor Limits</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_updateLimits">
<property name="text">
<string>Update</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_Y_max">
<property name="text">
<string>+ Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="label_Y_min">
<property name="text">
<string>- Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_X_min">
<property name="text">
<string>- X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_X_max">
<property name="text">
<string>+ X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_y_max">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="spinBox_y_min">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QDoubleSpinBox" name="spinBox_x_min">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QDoubleSpinBox" name="spinBox_x_max">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Plotting Options</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_max_points">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>5000</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Max Points</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_scatter_size">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>15</number>
</property>
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Scatter Size</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QPushButton" name="pushButton_update_config">
<property name="text">
<string>Update Settings</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_num_dim_points">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>N dim</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QPushButton" name="pushButton_enableGUI">
<property name="text">
<string>Enable Control GUI</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_queue">
<attribute name="title">
<string>Queue</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Work in progress</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_5">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Reset Queue</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="tableWidget_2">
<property name="enabled">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>queueID</string>
</property>
</column>
<column>
<property name="text">
<string>scan_id</string>
</property>
</column>
<column>
<property name="text">
<string>is_scan</string>
</property>
</column>
<column>
<property name="text">
<string>type</string>
</property>
</column>
<column>
<property name="text">
<string>scan_number</string>
</property>
</column>
<column>
<property name="text">
<string>IQ status</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsLayoutWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,107 @@
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
class SettingWidget(QWidget):
"""
Abstract class for a settings widget to enforce the implementation of the accept_changes and display_current_settings.
Can be used for toolbar actions to display the settings of a widget.
Args:
target_widget (QWidget): The widget that the settings will be taken from and applied to.
"""
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.target_widget = None
def set_target_widget(self, target_widget: QWidget):
self.target_widget = target_widget
@Slot()
def accept_changes(self):
"""
Accepts the changes made in the settings widget and applies them to the target widget.
"""
pass
@Slot(dict)
def display_current_settings(self, config_dict: dict):
"""
Displays the current settings of the target widget in the settings widget.
Args:
config_dict(dict): The current settings of the target widget.
"""
pass
class SettingsDialog(QDialog):
"""
Dialog to display and edit the settings of a widget with accept and cancel buttons.
Args:
parent (QWidget): The parent widget of the dialog.
target_widget (QWidget): The widget that the settings will be taken from and applied to.
settings_widget (SettingWidget): The widget that will display the settings.
"""
def __init__(
self,
parent=None,
settings_widget: SettingWidget = None,
window_title: str = "Settings",
config: dict = None,
*args,
**kwargs,
):
super().__init__(parent, *args, **kwargs)
self.setModal(False)
self.setWindowTitle(window_title)
self.widget = settings_widget
self.widget.set_target_widget(parent)
if config is None:
config = parent.get_config()
self.widget.display_current_settings(config)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.apply_button = QPushButton("Apply")
button_layout = QHBoxLayout()
button_layout.addWidget(self.button_box.button(QDialogButtonBox.Cancel))
button_layout.addWidget(self.apply_button)
button_layout.addWidget(self.button_box.button(QDialogButtonBox.Ok))
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.apply_button.clicked.connect(self.apply_changes)
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(5, 5, 5, 5)
self.layout.addWidget(self.widget)
self.layout.addLayout(button_layout)
ok_button = self.button_box.button(QDialogButtonBox.Ok)
ok_button.setDefault(True)
ok_button.setAutoDefault(True)
@Slot()
def accept(self):
"""
Accept the changes made in the settings widget and close the dialog.
"""
self.widget.accept_changes()
super().accept()
@Slot()
def apply_changes(self):
"""
Apply the changes made in the settings widget without closing the dialog.
"""
self.widget.accept_changes()

View File

@@ -3,8 +3,7 @@ from collections import defaultdict
# pylint: disable=no-name-in-module
from qtpy.QtCore import QSize
from qtpy.QtGui import QAction
from qtpy.QtWidgets import QHBoxLayout, QLabel, QSpinBox, QToolBar, QWidget
from qtpy.QtWidgets import QToolBar, QWidget
class ToolBarAction(ABC):
@@ -18,34 +17,6 @@ class ToolBarAction(ABC):
"""
class ColumnAdjustAction(ToolBarAction):
"""Toolbar spinbox to adjust number of columns in the plot layout"""
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
"""Creates a access history button for the toolbar.
Args:
toolbar (QToolBar): The toolbar to add the action to.
target (QWidget): The widget that the 'Access Scan History' action will be targeted.
Returns:
QAction: The 'Access Scan History' action created for the toolbar.
"""
widget = QWidget()
layout = QHBoxLayout(widget)
label = QLabel("Columns:")
spin_box = QSpinBox()
spin_box.setMinimum(1) # Set minimum value
spin_box.setMaximum(10) # Set maximum value
spin_box.setValue(target.get_column_count()) # Initial value
spin_box.valueChanged.connect(lambda value: target.set_column_count(value))
layout.addWidget(label)
layout.addWidget(spin_box)
toolbar.addWidget(widget)
class ModularToolBar(QToolBar):
"""Modular toolbar with optional automatic initialization.
Args:

View File

@@ -289,6 +289,8 @@ class BECConnector(BECWidget):
print("No more connections. Shutting down GUI BEC client.")
self.bec_dispatcher.disconnect_all()
self.client.shutdown()
if hasattr(super(), "cleanup"):
super().cleanup()
# def closeEvent(self, event):
# self.cleanup()

View File

@@ -1,2 +1,8 @@
class BECWidget:
"""Base class for all BEC widgets."""
def closeEvent(self, event):
if hasattr(self, "cleanup"):
self.cleanup()
if hasattr(super(), "closeEvent"):
super().closeEvent(event)

View File

@@ -58,11 +58,12 @@ class DesignerPluginGenerator:
os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
)
def run(self):
def run(self, validate=True):
if self._excluded:
print(f"Plugin {self.widget.__name__} is excluded from generation.")
return
self._check_class_validity()
if validate:
self._check_class_validity()
self._load_templates()
self._write_templates()
@@ -142,7 +143,7 @@ class DesignerPluginGenerator:
if __name__ == "__main__": # pragma: no cover
# from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
from bec_widgets.widgets.dock import BECDockArea
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
generator = DesignerPluginGenerator(BECDockArea)
generator.run()
generator = DesignerPluginGenerator(SpinnerWidget)
generator.run(validate=False)

View File

@@ -0,0 +1,92 @@
import os
import sys
from PIL import Image, ImageChops
from qtpy.QtGui import QPixmap
import bec_widgets
REFERENCE_DIR = os.path.join(
os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/references"
)
REFERENCE_DIR_FAILURES = os.path.join(
os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/reference_failures"
)
def compare_images(image1_path: str, reference_image_path: str):
"""
Load two images and compare them pixel by pixel
Args:
image1_path(str): The path to the first image
reference_image_path(str): The path to the reference image
Raises:
ValueError: If the images are different
"""
image1 = Image.open(image1_path)
image2 = Image.open(reference_image_path)
if image1.size != image2.size:
raise ValueError("Image size has changed")
diff = ImageChops.difference(image1, image2)
if diff.getbbox():
# copy image1 to the reference directory to upload as artifact
os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
image_name = os.path.join(REFERENCE_DIR_FAILURES, os.path.basename(image1_path))
image1.save(image_name)
print(f"Image saved to {image_name}")
raise ValueError("Images are different")
def snap_and_compare(widget: any, output_directory: str, suffix: str = ""):
"""
Save a rendering of a widget and compare it to a reference image
Args:
widget(any): The widget to render
output_directory(str): The directory to save the image to
suffix(str): A suffix to append to the image name
Raises:
ValueError: If the images are different
Examples:
snap_and_compare(widget, tmpdir, suffix="started")
"""
if not isinstance(output_directory, str):
output_directory = str(output_directory)
os_suffix = sys.platform
name = (
f"{widget.__class__.__name__}_{suffix}_{os_suffix}.png"
if suffix
else f"{widget.__class__.__name__}_{os_suffix}.png"
)
# Save the widget to a pixmap
test_image_path = os.path.join(output_directory, name)
pixmap = QPixmap(widget.size())
widget.render(pixmap)
pixmap.save(test_image_path)
try:
reference_path = os.path.join(REFERENCE_DIR, f"{widget.__class__.__name__}")
reference_image_path = os.path.join(reference_path, name)
if not os.path.exists(reference_image_path):
raise ValueError(f"Reference image not found: {reference_image_path}")
compare_images(test_image_path, reference_image_path)
except ValueError:
image = Image.open(test_image_path)
os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
image_name = os.path.join(REFERENCE_DIR_FAILURES, name)
image.save(image_name)
print(f"Image saved to {image_name}")
raise

View File

@@ -25,6 +25,7 @@ class DeviceInputBase(BECConnector):
super().__init__(client=client, config=config, gui_id=gui_id)
self.get_bec_shortcuts()
self._device_filter = None
self._devices = []
@property
@@ -56,6 +57,7 @@ class DeviceInputBase(BECConnector):
"""
self.validate_device_filter(device_filter)
self.config.device_filter = device_filter
self._device_filter = device_filter
def set_default_device(self, default_device: str):
"""

View File

@@ -27,7 +27,7 @@ class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return ""
return "BEC Core Widgets"
def icon(self):
return QIcon()

View File

@@ -27,7 +27,7 @@ class BECStatusBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return ""
return "BEC Core Widgets"
def icon(self):
return QIcon()

View File

@@ -1 +0,0 @@
from .stop_button.stop_button import StopButton

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -3,7 +3,7 @@ import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.buttons.color_button.color_button import ColorButton
from bec_widgets.widgets.color_button.color_button import ColorButton
DOM_XML = """
<ui language='c++'>

View File

@@ -6,7 +6,7 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.buttons.color_button.color_button_plugin import ColorButtonPlugin
from bec_widgets.widgets.color_button.color_button_plugin import ColorButtonPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(ColorButtonPlugin())

View File

@@ -222,7 +222,7 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
Start ``Backend`` process and render Pyte output as text.
"""
def __init__(self, parent, numColumns, numLines, **kwargs):
def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
super().__init__(parent)
# file descriptor to communicate with the subprocess

View File

@@ -0,0 +1,201 @@
import os
import uuid
from bec_lib.endpoints import MessageEndpoints
from bec_lib.messages import ScanQueueMessage
from qtpy.QtCore import Property, Signal, Slot
from qtpy.QtGui import QDoubleValidator
from qtpy.QtWidgets import QDoubleSpinBox, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_connector import BECConnector
class DeviceBox(BECConnector, QWidget):
"""A widget that controls a single device."""
ui_file = "device_box.ui"
dimensions = (234, 224)
device_changed = Signal(str, str)
def __init__(self, parent=None, device=None, *args, **kwargs):
super().__init__(*args, **kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts()
self._device = ""
self._limits = None
self.init_ui()
if device is not None:
self.device = device
self.init_device()
def init_ui(self):
self.device_changed.connect(self.on_device_change)
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
# fix the size of the device box
db = self.ui.device_box
db.setFixedHeight(self.dimensions[0])
db.setFixedWidth(self.dimensions[1])
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
self.ui.stop.clicked.connect(self.on_stop)
self.ui.tweak_right.clicked.connect(self.on_tweak_right)
self.ui.tweak_right.setToolTip("Tweak right")
self.ui.tweak_left.clicked.connect(self.on_tweak_left)
self.ui.tweak_left.setToolTip("Tweak left")
self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
self.setpoint_validator = QDoubleValidator()
self.ui.setpoint.setValidator(self.setpoint_validator)
self.ui.spinner_widget.start()
def init_device(self):
if self.device in self.dev:
data = self.dev[self.device].read()
self.on_device_readback({"signals": data}, {})
@Property(str)
def device(self):
return self._device
@device.setter
def device(self, value):
if not value or not isinstance(value, str):
return
old_device = self._device
self._device = value
self.device_changed.emit(old_device, value)
@Slot(str, str)
def on_device_change(self, old_device: str, new_device: str):
if new_device not in self.dev:
print(f"Device {new_device} not found in the device list")
return
print(f"Device changed from {old_device} to {new_device}")
self.init_device()
self.bec_dispatcher.disconnect_slot(
self.on_device_readback, MessageEndpoints.device_readback(old_device)
)
self.bec_dispatcher.connect_slot(
self.on_device_readback, MessageEndpoints.device_readback(new_device)
)
self.ui.device_box.setTitle(new_device)
self.ui.readback.setToolTip(f"{self.device} readback")
self.ui.setpoint.setToolTip(f"{self.device} setpoint")
self.ui.step_size.setToolTip(f"Step size for {new_device}")
precision = self.dev[new_device].precision
if precision is not None:
self.ui.step_size.setDecimals(precision)
self.ui.step_size.setValue(10**-precision * 10)
@Slot(dict, dict)
def on_device_readback(self, msg_content: dict, metadata: dict):
signals = msg_content.get("signals", {})
# pylint: disable=protected-access
hinted_signals = self.dev[self.device]._hints
precision = self.dev[self.device].precision
readback_val = None
setpoint_val = None
if len(hinted_signals) == 1:
signal = hinted_signals[0]
readback_val = signals.get(signal, {}).get("value")
if f"{self.device}_setpoint" in signals:
setpoint_val = signals.get(f"{self.device}_setpoint", {}).get("value")
if f"{self.device}_motor_is_moving" in signals:
is_moving = signals.get(f"{self.device}_motor_is_moving", {}).get("value")
if is_moving:
self.ui.spinner_widget.start()
self.ui.spinner_widget.setToolTip("Device is moving")
else:
self.ui.spinner_widget.stop()
self.ui.spinner_widget.setToolTip("Device is idle")
if readback_val is not None:
self.ui.readback.setText(f"{readback_val:.{precision}f}")
if setpoint_val is not None:
self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")
limits = self.dev[self.device].limits
self.update_limits(limits)
if limits is not None and readback_val is not None and limits[0] != limits[1]:
pos = (readback_val - limits[0]) / (limits[1] - limits[0])
self.ui.position_indicator.on_position_update(pos)
def update_limits(self, limits):
if limits == self._limits:
return
self._limits = limits
if limits is not None and limits[0] != limits[1]:
self.ui.position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
self.setpoint_validator.setRange(limits[0], limits[1])
else:
self.ui.position_indicator.setToolTip("No limits set")
self.setpoint_validator.setRange(float("-inf"), float("inf"))
@Slot()
def on_stop(self):
request_id = str(uuid.uuid4())
params = {
"device": self.device,
"rpc_id": request_id,
"func": "stop",
"args": [],
"kwargs": {},
}
msg = ScanQueueMessage(
scan_type="device_rpc",
parameter=params,
queue="emergency",
metadata={"RID": request_id, "response": False},
)
self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
@property
def step_size(self):
return self.ui.step_size.value()
@Slot()
def on_tweak_right(self):
self.dev[self.device].move(self.step_size, relative=True)
@Slot()
def on_tweak_left(self):
self.dev[self.device].move(-self.step_size, relative=True)
@Slot()
def on_setpoint_change(self):
self.ui.setpoint.clearFocus()
setpoint = self.ui.setpoint.text()
self.dev[self.device].move(float(setpoint), relative=False)
self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
if __name__ == "__main__": # pragma: no cover
import sys
import qdarktheme
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
qdarktheme.setup_theme("light")
widget = DeviceBox(device="samx")
widget.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1 @@
{'files': ['device_box.py']}

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>251</width>
<height>289</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>192</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="device_box">
<property name="title">
<string>Device Name</string>
</property>
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0">
<property name="topMargin">
<number>0</number>
</property>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="step_size"/>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="tweak_right">
<property name="minimumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::ArrowType::RightArrow</enum>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QLineEdit" name="setpoint"/>
</item>
<item row="3" column="0">
<widget class="QToolButton" name="tweak_left">
<property name="minimumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::ArrowType::LeftArrow</enum>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QPushButton" name="stop">
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="SpinnerWidget" name="spinner_widget">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="PositionIndicator" name="position_indicator"/>
</item>
<item>
<widget class="QLabel" name="readback">
<property name="text">
<string>Position</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SpinnerWidget</class>
<extends>QWidget</extends>
<header>spinner_widget</header>
</customwidget>
<customwidget>
<class>PositionIndicator</class>
<extends>QWidget</extends>
<header>position_indicator</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.device_box.device_box import DeviceBox
DOM_XML = """
<ui language='c++'>
<widget class='DeviceBox' name='device_box'>
</widget>
</ui>
"""
class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = DeviceBox(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "Device Control"
def icon(self):
return QIcon()
def includeFile(self):
return "device_box"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "DeviceBox"
def toolTip(self):
return "A widget for controlling a single positioner. "
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,22 @@
from bec_widgets.widgets.device_box.device_box import DeviceBox
class DeviceControlLine(DeviceBox):
"""A widget that controls a single device."""
ui_file = "device_control_line.ui"
dimensions = (70, 800) # height, width
if __name__ == "__main__": # pragma: no cover
import sys
import qdarktheme
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
qdarktheme.setup_theme("light")
widget = DeviceControlLine(device="samy")
widget.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>785</width>
<height>91</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="device_box">
<property name="title">
<string>Device Name</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="PositionIndicator" name="position_indicator"/>
</item>
<item>
<widget class="QLabel" name="readback">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Position</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="setpoint">
<property name="minimumSize">
<size>
<width>150</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="stop">
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="tweak_left">
<property name="minimumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::ArrowType::LeftArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="step_size"/>
</item>
<item>
<widget class="QToolButton" name="tweak_right">
<property name="minimumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::ArrowType::RightArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="SpinnerWidget" name="spinner_widget">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SpinnerWidget</class>
<extends>QWidget</extends>
<header>spinner_widget</header>
</customwidget>
<customwidget>
<class>PositionIndicator</class>
<extends>QWidget</extends>
<header>position_indicator</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.device_box.device_box_plugin import DeviceBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceBoxPlugin())
if __name__ == "__main__": # pragma: no cover
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -2,10 +2,10 @@ from typing import TYPE_CHECKING
from qtpy.QtWidgets import QComboBox
from bec_widgets.widgets.device_inputs.device_input_base import DeviceInputBase, DeviceInputConfig
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase, DeviceInputConfig
if TYPE_CHECKING:
from bec_widgets.widgets.device_inputs.device_input_base import DeviceInputConfig
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig
class DeviceComboBox(DeviceInputBase, QComboBox):

View File

@@ -0,0 +1,3 @@
{
"files": ["device_combobox.py"]
}

View File

@@ -1,10 +1,11 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.device_inputs import DeviceComboBox
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
DOM_XML = """
<ui language='c++'>
@@ -27,10 +28,12 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return ""
return "BEC Device Inputs"
def icon(self):
return QIcon()
current_path = os.path.dirname(__file__)
icon_path = os.path.join(current_path, "assets", "device_combobox_icon.png")
return QIcon(icon_path)
def includeFile(self):
return "device_combobox"

View File

@@ -6,9 +6,7 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.device_inputs.device_combobox.device_combobox_plugin import (
DeviceComboBoxPlugin,
)
from bec_widgets.widgets.device_combobox.device_combobox_plugin import DeviceComboBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceComboBoxPlugin())

View File

@@ -1,2 +0,0 @@
from .device_combobox.device_combobox import DeviceComboBox
from .device_line_edit.device_line_edit import DeviceLineEdit

View File

@@ -1,4 +0,0 @@
{
"files": ["device_combobox.py", "launch_device_combobox.py",
]
}

View File

@@ -1,11 +0,0 @@
from bec_widgets.widgets.device_inputs import DeviceComboBox
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
w = DeviceComboBox()
w.show()
sys.exit(app.exec_())

View File

@@ -1,4 +0,0 @@
{
"files": ["device_line_edit.py", "launch_device_line_edit.py",
]
}

View File

@@ -1,11 +0,0 @@
from bec_widgets.widgets.device_inputs import DeviceLineEdit
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
w = DeviceLineEdit()
w.show()
sys.exit(app.exec_())

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -3,10 +3,10 @@ from typing import TYPE_CHECKING
from qtpy.QtCore import QSize
from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy
from bec_widgets.widgets.device_inputs.device_input_base import DeviceInputBase, DeviceInputConfig
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase, DeviceInputConfig
if TYPE_CHECKING:
from bec_widgets.widgets.device_inputs.device_input_base import DeviceInputConfig
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig
class DeviceLineEdit(DeviceInputBase, QLineEdit):

View File

@@ -0,0 +1,3 @@
{
"files": ["device_line_edit.py"]
}

View File

@@ -1,10 +1,11 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.device_inputs import DeviceLineEdit
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
DOM_XML = """
<ui language='c++'>
@@ -27,10 +28,12 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return ""
return "BEC Device Inputs"
def icon(self):
return QIcon()
current_path = os.path.dirname(__file__)
icon_path = os.path.join(current_path, "assets", "line_edit_icon.png")
return QIcon(icon_path)
def includeFile(self):
return "device_line_edit"

View File

@@ -6,9 +6,7 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.device_inputs.device_line_edit.device_line_edit_plugin import (
DeviceLineEditPlugin,
)
from bec_widgets.widgets.device_line_edit.device_line_edit_plugin import DeviceLineEditPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceLineEditPlugin())

View File

@@ -830,6 +830,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
)
def cleanup(self):
self.clear_all()
super().cleanup()
# def cleanup(self):
# self.clear_all()
# super().cleanup()

View File

@@ -271,11 +271,12 @@ class BECMotorMap(BECPlotBase):
def _swap_limit_map(self):
"""Swap the limit map."""
self.plot_item.removeItem(self.plot_components["limit_map"])
self.plot_components["limit_map"] = self._make_limit_map(
self.config.signals.x.limits, self.config.signals.y.limits
)
self.plot_components["limit_map"].setZValue(-1)
self.plot_item.addItem(self.plot_components["limit_map"])
if self.config.signals.x.limits is not None and self.config.signals.y.limits is not None:
self.plot_components["limit_map"] = self._make_limit_map(
self.config.signals.x.limits, self.config.signals.y.limits
)
self.plot_components["limit_map"].setZValue(-1)
self.plot_item.addItem(self.plot_components["limit_map"])
def _make_motor_map(self):
"""
@@ -284,9 +285,10 @@ class BECMotorMap(BECPlotBase):
# Create limit map
motor_x_limit = self.config.signals.x.limits
motor_y_limit = self.config.signals.y.limits
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
self.plot_item.addItem(self.plot_components["limit_map"])
self.plot_components["limit_map"].setZValue(-1)
if motor_x_limit is not None or motor_y_limit is not None:
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
self.plot_item.addItem(self.plot_components["limit_map"])
self.plot_components["limit_map"].setZValue(-1)
# Create scatter plot
scatter_size = self.config.scatter_size

View File

@@ -221,6 +221,7 @@ class BECWaveform(BECPlotBase):
label: str | None = None,
validate: bool = True,
dap: str | None = None, # TODO add dap custom curve wrapper
**kwargs,
) -> BECCurve:
"""
Plot a curve to the plot widget.
@@ -244,7 +245,7 @@ class BECWaveform(BECPlotBase):
"""
if x is not None and y is not None:
return self.add_curve_custom(x=x, y=y, label=label, color=color)
return self.add_curve_custom(x=x, y=y, label=label, color=color, **kwargs)
else:
if dap:
self.add_dap(x_name=x_name, y_name=y_name, dap=dap)
@@ -259,6 +260,7 @@ class BECWaveform(BECPlotBase):
color_map_z=color_map_z,
label=label,
validate_bec=validate,
**kwargs,
)
def add_curve_custom(
@@ -754,7 +756,11 @@ class BECWaveform(BECPlotBase):
self.update_dap, MessageEndpoints.dap_response(self.scan_id)
)
if scan_index is not None:
self.scan_id = self.queue.scan_storage.storage[scan_index].scan_id
try:
self.scan_id = self.queue.scan_storage.storage[scan_index].scan_id
except IndexError:
print(f"Scan index {scan_index} out of range.")
return
elif scan_id is not None:
self.scan_id = scan_id

View File

@@ -1,252 +0,0 @@
# pylint: disable = no-name-in-module,missing-module-docstring
from enum import Enum
from bec_lib.alarm_handler import AlarmBase
from bec_lib.device import Positioner
from qtpy.QtCore import QThread
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QMessageBox, QWidget
from bec_widgets.utils.bec_dispatcher import BECDispatcher
CONFIG_DEFAULT = {
"motor_control": {
"motor_x": "samx",
"motor_y": "samy",
"step_size_x": 3,
"step_size_y": 50,
"precision": 4,
"step_x_y_same": False,
"move_with_arrows": False,
}
}
class MotorControlWidget(QWidget):
"""Base class for motor control widgets."""
def __init__(self, parent=None, client=None, motor_thread=None, config=None):
super().__init__(parent)
self.client = client
self.motor_thread = motor_thread
self.config = config
self.motor_x = None
self.motor_y = None
if not self.client:
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client
if not self.motor_thread:
self.motor_thread = MotorThread(client=self.client)
self._load_ui()
if self.config is None:
print(f"No initial config found for {self.__class__.__name__}")
self._init_ui()
else:
self.on_config_update(self.config)
def _load_ui(self):
"""Load the UI from the .ui file."""
def _init_ui(self):
"""Initialize the UI components specific to the widget."""
@pyqtSlot(dict)
def on_config_update(self, config):
"""Handle configuration updates."""
self.config = config
self._init_ui()
class MotorControlErrors:
"""Class for displaying formatted error messages."""
@staticmethod
def display_error_message(error_message: str) -> None:
"""
Display a critical error message.
Args:
error_message(str): Error message to display.
"""
# Create a QMessageBox
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setWindowTitle("Critical Error")
# Format the message
formatted_message = MotorControlErrors._format_error_message(error_message)
msg.setText(formatted_message)
# Display the message box
msg.exec_()
@staticmethod
def _format_error_message(error_message: str) -> str:
"""
Format the error message.
Args:
error_message(str): Error message to format.
Returns:
str: Formatted error message.
"""
# Split the message into lines
lines = error_message.split("\n")
formatted_lines = [
f"<b>{line.strip()}</b>" if i == 0 else line.strip()
for i, line in enumerate(lines)
if line.strip()
]
# Join the lines with double breaks for empty lines in between
formatted_message = "<br><br>".join(formatted_lines)
return formatted_message
class MotorActions(Enum):
"""Enum for motor actions."""
MOVE_ABSOLUTE = "move_absolute"
MOVE_RELATIVE = "move_relative"
class MotorThread(QThread):
"""
QThread subclass for controlling motor actions asynchronously.
Signals:
coordinates_updated (pyqtSignal): Signal to emit current coordinates.
motor_error (pyqtSignal): Signal to emit when there is an error with the motors.
lock_gui (pyqtSignal): Signal to lock/unlock the GUI.
"""
coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates
motor_error = pyqtSignal(str) # Signal to emit when there is an error with the motors
lock_gui = pyqtSignal(bool) # Signal to lock/unlock the GUI
def __init__(self, parent=None, client=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.dev = self.client.device_manager.devices
self.scans = self.client.scans
self.queue = self.client.queue
self.action = None
self.motor = None
self.motor_x = None
self.motor_y = None
self.target_coordinates = None
self.value = None
def get_all_motors_names(self) -> list:
"""
Get all the motors names.
Returns:
list: List of all the motors names.
"""
all_devices = self.client.device_manager.devices.enabled_devices
all_motors_names = [motor.name for motor in all_devices if isinstance(motor, Positioner)]
return all_motors_names
def get_coordinates(self, motor_x: str, motor_y: str) -> tuple:
"""
Get the current coordinates of the motors.
Args:
motor_x(str): Motor X to get positions from.
motor_y(str): Motor Y to get positions from.
Returns:
tuple: Current coordinates of the motors.
"""
x = self.dev[motor_x].readback.get()
y = self.dev[motor_y].readback.get()
return x, y
def move_absolute(self, motor_x: str, motor_y: str, target_coordinates: tuple) -> None:
"""
Wrapper for moving the motor to the target coordinates.
Args:
motor_x(str): Motor X to move.
motor_y(str): Motor Y to move.
target_coordinates(tuple): Target coordinates.
"""
self.action = MotorActions.MOVE_ABSOLUTE
self.motor_x = motor_x
self.motor_y = motor_y
self.target_coordinates = target_coordinates
self.start()
def move_relative(self, motor: str, value: float) -> None:
"""
Wrapper for moving the motor relative to the current position.
Args:
motor(str): Motor to move.
value(float): Value to move.
"""
self.action = MotorActions.MOVE_RELATIVE
self.motor = motor
self.value = value
self.start()
def run(self):
"""
Run the thread.
Possible actions:
- Move to coordinates
- Move relative
"""
if self.action == MotorActions.MOVE_ABSOLUTE:
self._move_motor_absolute(self.motor_x, self.motor_y, self.target_coordinates)
elif self.action == MotorActions.MOVE_RELATIVE:
self._move_motor_relative(self.motor, self.value)
def _move_motor_absolute(self, motor_x: str, motor_y: str, target_coordinates: tuple) -> None:
"""
Move the motor to the target coordinates.
Args:
motor_x(str): Motor X to move.
motor_y(str): Motor Y to move.
target_coordinates(tuple): Target coordinates.
"""
self.lock_gui.emit(False)
try:
status = self.scans.mv(
self.dev[motor_x],
target_coordinates[0],
self.dev[motor_y],
target_coordinates[1],
relative=False,
)
status.wait()
except AlarmBase as e:
self.motor_error.emit(str(e))
finally:
self.lock_gui.emit(True)
def _move_motor_relative(self, motor, value: float) -> None:
"""
Move the motor relative to the current position.
Args:
motor(str): Motor to move.
value(float): Value to move.
"""
self.lock_gui.emit(False)
try:
status = self.scans.mv(self.dev[motor], value, relative=True)
status.wait()
except AlarmBase as e:
self.motor_error.emit(str(e))
finally:
self.lock_gui.emit(True)
def stop_movement(self):
self.queue.request_scan_abortion()
self.queue.request_queue_reset()

View File

@@ -1,484 +0,0 @@
# pylint: disable = no-name-in-module,missing-module-docstring
import os
from qtpy import uic
from qtpy.QtCore import Qt
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QDoubleValidator, QKeySequence
from qtpy.QtWidgets import (
QCheckBox,
QLineEdit,
QMessageBox,
QPushButton,
QShortcut,
QTableWidget,
QTableWidgetItem,
)
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
class MotorCoordinateTable(MotorControlWidget):
"""
Widget to save coordinates from motor, display them in the table and move back to them.
There are two modes of operation:
- Individual: Each row is a single coordinate.
- Start/Stop: Each pair of rows is a start and end coordinate.
Signals:
plot_coordinates_signal (pyqtSignal(list, str, str)): Signal to plot the coordinates in the MotorMap.
Slots:
add_coordinate (pyqtSlot(tuple)): Slot to add a coordinate to the table.
mode_switch (pyqtSlot): Slot to switch between individual and start/stop mode.
"""
plot_coordinates_signal = pyqtSignal(list, str, str)
def _load_ui(self):
"""Load the UI for the coordinate table."""
current_path = os.path.dirname(__file__)
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.ui.table.setSelectionBehavior(QTableWidget.SelectRows)
# for tag columns default tag
self.tag_counter = 1
# Connect signals and slots
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.ui.table)
self.delete_shortcut.activated.connect(self.delete_selected_row)
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
self.warning_message = True
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""
Update config dict
Args:
config(dict): New config dict
"""
self.config = config
# Get motor names
self.motor_x, self.motor_y = (
self.config["motor_control"]["motor_x"],
self.config["motor_control"]["motor_y"],
)
# Decimal precision of the table coordinates
self.precision = self.config["motor_control"].get("precision", 3)
# Mode switch default option
self.mode = self.config["motor_control"].get("mode", "Individual")
# Set combobox to default 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.ui.comboBox_mode.currentText()
if mode == "Individual":
self._setup_individual_mode()
elif mode == "Start/Stop":
self._setup_start_stop_mode()
self.start_stop_counter = 0 # TODO: remove this??
self.wipe_motor_map_coordinates()
def _setup_individual_mode(self):
"""Setup the table for individual mode."""
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.ui.table.setColumnCount(8)
self.ui.table.setHorizontalHeaderLabels(
[
"Show",
"Move [start]",
"Move [end]",
"Tag",
"X [start]",
"Y [start]",
"X [end]",
"Y [end]",
]
)
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.ui.comboBox_mode.currentIndex()
if self.ui.table.rowCount() > 0 and self.warning_message is True:
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Critical)
msgBox.setText(
"Switching modes will delete all table entries. Do you want to continue?"
)
msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
returnValue = msgBox.exec()
if returnValue is QMessageBox.Cancel:
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
self.wipe_motor_map_coordinates()
# Initiate new table with new mode
self._setup_table()
@pyqtSlot(tuple)
def add_coordinate(self, coordinates: tuple):
"""
Add a coordinate to the table.
Args:
coordinates(tuple): Coordinates (x,y) to add to the table.
"""
tag = f"Pos {self.tag_counter}"
self.tag_counter += 1
x, y = coordinates
self._add_row(tag, x, y)
def _add_row(self, tag: str, x: float, y: float) -> None:
"""
Add a row to the table.
Args:
tag(str): Tag of the coordinate.
x(float): X coordinate.
y(float): Y coordinate.
"""
mode = self.ui.comboBox_mode.currentText()
if mode == "Individual":
checkbox_pos = 0
button_pos = 1
tag_pos = 2
x_pos = 3
y_pos = 4
coordinate_reference = "Individual"
color = "green"
# Add new row -> new entry
row_count = self.ui.table.rowCount()
self.ui.table.insertRow(row_count)
# Add Widgets
self._add_widgets(
tag,
x,
y,
row_count,
checkbox_pos,
tag_pos,
button_pos,
x_pos,
y_pos,
coordinate_reference,
color,
)
if mode == "Start/Stop":
# These positions are always fixed
checkbox_pos = 0
tag_pos = 3
if self.is_next_entry_end is False: # It is the start position of the entry
print("Start position")
button_pos = 1
x_pos = 4
y_pos = 5
coordinate_reference = "Start"
color = "blue"
# Add new row -> new entry
row_count = self.ui.table.rowCount()
self.ui.table.insertRow(row_count)
# Add Widgets
self._add_widgets(
tag,
x,
y,
row_count,
checkbox_pos,
tag_pos,
button_pos,
x_pos,
y_pos,
coordinate_reference,
color,
)
# Next entry will be the end of the current entry
self.is_next_entry_end = True
elif self.is_next_entry_end is True: # It is the end position of the entry
print("End position")
row_count = self.ui.table.rowCount() - 1 # Current row
button_pos = 2
x_pos = 6
y_pos = 7
coordinate_reference = "Stop"
color = "red"
# Add Widgets
self._add_widgets(
tag,
x,
y,
row_count,
checkbox_pos,
tag_pos,
button_pos,
x_pos,
y_pos,
coordinate_reference,
color,
)
self.is_next_entry_end = False # Next entry will be the start of the new entry
# Auto table resize
self.resize_table_auto()
def _add_widgets(
self,
tag: str,
x: float,
y: float,
row: int,
checkBox_pos: int,
tag_pos: int,
button_pos: int,
x_pos: int,
y_pos: int,
coordinate_reference: str,
color: str,
) -> None:
"""
Add widgets to the table.
Args:
tag(str): Tag of the coordinate.
x(float): X coordinate.
y(float): Y coordinate.
row(int): Row of the QTableWidget where to add the widgets.
checkBox_pos(int): Column where to put CheckBox.
tag_pos(int): Column where to put Tag.
button_pos(int): Column where to put Move button.
x_pos(int): Column where to link x coordinate.
y_pos(int): Column where to link y coordinate.
coordinate_reference(str): Reference to the coordinate for MotorMap.
color(str): Color of the coordinate for MotorMap.
"""
# Add widgets
self._add_checkbox(row, checkBox_pos, x_pos, y_pos)
self._add_move_button(row, button_pos, x_pos, y_pos)
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)
# # Emit the coordinates to be plotted
self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
# Connect item edit to emit coordinates
self.ui.table.itemChanged.connect(
lambda: print(f"item changed from {coordinate_reference} slot \n {x}-{y}-{color}")
)
self.ui.table.itemChanged.connect(
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
)
def _add_checkbox(self, row: int, checkBox_pos: int, x_pos: int, y_pos: int):
"""
Add a checkbox to the table.
Args:
row(int): Row of QTableWidget where to add the checkbox.
checkBox_pos(int): Column where to put CheckBox.
x_pos(int): Column where to link x coordinate.
y_pos(int): Column where to link y coordinate.
"""
show_checkbox = QCheckBox()
show_checkbox.setChecked(True)
show_checkbox.stateChanged.connect(lambda: self.emit_plot_coordinates(x_pos, y_pos))
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:
"""
Add a move button to the table.
Args:
row(int): Row of QTableWidget where to add the move button.
button_pos(int): Column where to put move button.
x_pos(int): Column where to link x coordinate.
y_pos(int): Column where to link y coordinate.
"""
move_button = QPushButton("Move")
move_button.clicked.connect(lambda: self.handle_move_button_click(x_pos, y_pos))
self.ui.table.setCellWidget(row, button_pos, move_button)
def _add_line_edit(
self,
value: float,
row: int,
line_pos: int,
x_pos: int,
y_pos: int,
coordinate_reference: str,
color: str,
) -> None:
"""
Add a QLineEdit to the table.
Args:
value(float): Initial value of the QLineEdit.
row(int): Row of QTableWidget where to add the QLineEdit.
line_pos(int): Column where to put QLineEdit.
x_pos(int): Column where to link x coordinate.
y_pos(int): Column where to link y coordinate.
coordinate_reference(str): Reference to the coordinate for MotorMap.
color(str): Color of the coordinate for MotorMap.
"""
# Adding validator
validator = QDoubleValidator()
validator.setDecimals(self.precision)
# Create line edit
edit = QLineEdit(str(f"{value:.{self.precision}f}"))
edit.setValidator(validator)
edit.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Add line edit to the table
self.ui.table.setCellWidget(row, line_pos, edit)
edit.textChanged.connect(
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
)
def wipe_motor_map_coordinates(self):
"""Wipe the motor map coordinates."""
try:
self.ui.table.itemChanged.disconnect() # Disconnect all previous connections
except TypeError:
print("No previous connections to disconnect")
self.ui.table.setRowCount(0)
reference_tags = ["Individual", "Start", "Stop"]
for reference_tag in reference_tags:
self.plot_coordinates_signal.emit([], reference_tag, "green")
def handle_move_button_click(self, x_pos: int, y_pos: int) -> None:
"""
Handle the move button click.
Args:
x_pos(int): X position of the coordinate.
y_pos(int): Y position of the coordinate.
"""
button = self.sender()
row = self.ui.table.indexAt(button.pos()).row()
x = self.get_coordinate(row, x_pos)
y = self.get_coordinate(row, y_pos)
self.move_motor(x, y)
def emit_plot_coordinates(self, x_pos: float, y_pos: float, reference_tag: str, color: str):
"""
Emit the coordinates to be plotted.
Args:
x_pos(float): X position of the coordinate.
y_pos(float): Y position of the coordinate.
reference_tag(str): Reference tag of the coordinate.
color(str): Color of the coordinate.
"""
print(
f"Emitting plot coordinates: x_pos={x_pos}, y_pos={y_pos}, reference_tag={reference_tag}, color={color}"
)
coordinates = []
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)
coordinates.append((x, y, show)) # (x, y, show_flag)
self.plot_coordinates_signal.emit(coordinates, reference_tag, color)
def get_coordinate(self, row: int, column: int) -> float:
"""
Helper function to get the coordinate from the table QLineEdit cells.
Args:
row(int): Row of the table.
column(int): Column of the table.
Returns:
float: Value of the coordinate.
"""
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.ui.table.selectionModel().selectedRows()
for row in selected_rows:
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.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.ui.checkBox_resize_auto.isChecked():
self.ui.table.resizeColumnsToContents()
def move_motor(self, x: float, y: float) -> None:
"""
Move the motor to the target coordinates.
Args:
x(float): Target x coordinate.
y(float): Target y coordinate.
"""
self.motor_thread.move_absolute(self.motor_x, self.motor_y, (x, y))
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str) -> None:
"""
Change the active motors and update config.
Can be connected to the selected_motors_signal from MotorControlSelection.
Args:
motor_x(str): New motor X to be controlled.
motor_y(str): New motor Y to be controlled.
"""
self.motor_x = motor_x
self.motor_y = motor_y
self.config["motor_control"]["motor_x"] = motor_x
self.config["motor_control"]["motor_y"] = motor_y
@pyqtSlot(int)
def set_precision(self, precision: int) -> None:
"""
Set the precision of the coordinates.
Args:
precision(int): Precision of the coordinates.
"""
self.precision = precision
self.config["motor_control"]["precision"] = precision

View File

@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>676</width>
<height>667</height>
</rect>
</property>
<property name="windowTitle">
<string>Motor Coordinates Table</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="checkBox_resize_auto">
<property name="text">
<string>Resize Auto</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_editColumns">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Edit Custom Column</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Entries Mode:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<property name="enabled">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>Individual</string>
</property>
</item>
<item>
<property name="text">
<string>Start/Stop</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="table">
<property name="gridStyle">
<enum>Qt::SolidLine</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pushButton_importCSV">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Import CSV</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_exportCSV">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Export CSV</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,159 +0,0 @@
import os
from qtpy import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlErrors, MotorControlWidget
class MotorControlAbsolute(MotorControlWidget):
"""
Widget for controlling the motors to absolute coordinates.
Signals:
coordinates_signal (pyqtSignal(tuple)): Signal to emit the coordinates.
Slots:
change_motors (pyqtSlot): Slot to change the active motors.
enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls.
"""
coordinates_signal = pyqtSignal(tuple)
def _load_ui(self):
"""Load the UI from the .ui file."""
current_path = os.path.dirname(__file__)
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.ui.motorControl_absolute.setEnabled(False)
return
# Move to absolute coordinates
self.ui.pushButton_go_absolute.clicked.connect(
lambda: self.move_motor_absolute(
self.ui.spinBox_absolute_x.value(), self.ui.spinBox_absolute_y.value()
)
)
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)
# Error messages
self.motor_thread.motor_error.connect(
lambda error: MotorControlErrors.display_error_message(error)
)
# Keyboard shortcuts
self._init_keyboard_shortcuts()
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""Update config dict"""
self.config = config
# Get motor names
self.motor_x, self.motor_y = (
self.config["motor_control"]["motor_x"],
self.config["motor_control"]["motor_y"],
)
# Update step precision
self.precision = self.config["motor_control"]["precision"]
self._init_ui()
@pyqtSlot(bool)
def enable_motor_controls(self, enable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
enable(bool): True to enable, False to disable.
"""
# Disable or enable all controls within the motorControl_absolute group box
for widget in self.ui.motorControl_absolute.findChildren(QWidget):
widget.setEnabled(enable)
# Enable the pushButton_stop if the motor is moving
self.ui.pushButton_stop.setEnabled(True)
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str):
"""
Change the active motors and update config.
Can be connected to the selected_motors_signal from MotorControlSelection.
Args:
motor_x(str): New motor X to be controlled.
motor_y(str): New motor Y to be controlled.
"""
self.motor_x = motor_x
self.motor_y = motor_y
self.config["motor_control"]["motor_x"] = motor_x
self.config["motor_control"]["motor_y"] = motor_y
@pyqtSlot(int)
def set_precision(self, precision: int) -> None:
"""
Set the precision of the coordinates.
Args:
precision(int): Precision of the coordinates.
"""
self.precision = precision
self.config["motor_control"]["precision"] = 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:
"""
Move the motor to the target coordinates.
Args:
x(float): Target x coordinate.
y(float): Target y coordinate.
"""
# self._enable_motor_controls(False)
target_coordinates = (x, y)
self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates)
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.ui.pushButton_go_absolute.setShortcut("Ctrl+G")
self.ui.pushButton_go_absolute.setToolTip("Ctrl+G")
# Set absolute coordinates
self.ui.pushButton_set.setShortcut("Ctrl+D")
self.ui.pushButton_set.setToolTip("Ctrl+D")
# Save Current coordinates
self.ui.pushButton_save.setShortcut("Ctrl+S")
self.ui.pushButton_save.setToolTip("Ctrl+S")
# Stop Button
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.ui.spinBox_absolute_x.value(), self.precision), round(
self.ui.spinBox_absolute_y.value(), self.precision
)
self.coordinates_signal.emit((x, y))
def save_current_coordinates(self):
"""Emit the current coordinates from the motor thread"""
x, y = self.motor_thread.get_coordinates(self.motor_x, self.motor_y)
self.coordinates_signal.emit((round(x, self.precision), round(y, self.precision)))

View File

@@ -1,149 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>285</width>
<height>220</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>285</width>
<height>220</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>285</width>
<height>220</height>
</size>
</property>
<property name="windowTitle">
<string>Move Movement Absolute</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="motorControl_absolute">
<property name="minimumSize">
<size>
<width>261</width>
<height>195</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>261</width>
<height>195</height>
</size>
</property>
<property name="title">
<string>Move Movement Absolute</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox_save_with_go">
<property name="text">
<string>Save position with Go</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_absolute_y">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QDoubleSpinBox" name="spinBox_absolute_x">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="pushButton_save">
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_set">
<property name="text">
<string>Set</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_go_absolute">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="pushButton_stop">
<property name="text">
<string>Stop Movement</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,230 +0,0 @@
import os
from qtpy import uic
from qtpy.QtCore import Qt
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import QDoubleSpinBox, QShortcut, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
class MotorControlRelative(MotorControlWidget):
"""
Widget for controlling the motors to relative coordinates.
Signals:
precision_signal (pyqtSignal): Signal to emit the precision of the coordinates.
Slots:
change_motors (pyqtSlot(str,str)): Slot to change the active motors.
enable_motor_controls (pyqtSlot): Slot to enable/disable the motor controls.
"""
precision_signal = pyqtSignal(int)
def _load_ui(self):
"""Load the UI from the .ui file."""
# Loading UI
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_relative.ui"), self)
def _init_ui(self):
"""Initialize the UI."""
self._init_ui_motor_control()
self._init_keyboard_shortcuts()
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""
Update config dict
Args:
config(dict): New config dict
"""
self.config = config
# Get motor names
self.motor_x, self.motor_y = (
self.config["motor_control"]["motor_x"],
self.config["motor_control"]["motor_y"],
)
# Update step precision
self.precision = self.config["motor_control"]["precision"]
self.ui.spinBox_precision.setValue(self.precision)
# Update step sizes
self.ui.spinBox_step_x.setValue(self.config["motor_control"]["step_size_x"])
self.ui.spinBox_step_y.setValue(self.config["motor_control"]["step_size_y"])
# Checkboxes for keyboard shortcuts and x/y step size link
self.ui.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"])
self.ui.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"])
self._init_ui()
def _init_ui_motor_control(self) -> None:
"""Initialize the motor control elements"""
# Connect checkbox and spinBoxes
self.ui.checkBox_same_xy.stateChanged.connect(self._sync_step_sizes)
self.ui.spinBox_step_x.valueChanged.connect(self._update_step_size_x)
self.ui.spinBox_step_y.valueChanged.connect(self._update_step_size_y)
self.ui.toolButton_right.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, "x", 1)
)
self.ui.toolButton_left.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, "x", -1)
)
self.ui.toolButton_up.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, "y", 1)
)
self.ui.toolButton_down.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, "y", -1)
)
# Switch between key shortcuts active
self.ui.checkBox_enableArrows.stateChanged.connect(self._update_arrow_key_shortcuts)
self._update_arrow_key_shortcuts()
# Enable/Disable GUI
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
# Precision update
self.ui.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x))
# Error messages
self.motor_thread.motor_error.connect(
lambda error: MotorControlErrors.display_error_message(error)
)
# Stop Button
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
def _init_keyboard_shortcuts(self) -> None:
"""Initialize the keyboard shortcuts"""
# Increase/decrease step size for X motor
increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
increase_x_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_x, 2)
)
decrease_x_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_x, 0.5)
)
self.ui.spinBox_step_x.setToolTip("Increase step size: Ctrl+A\nDecrease step size: Ctrl+Z")
# Increase/decrease step size for Y motor
increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self)
decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self)
increase_y_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_y, 2)
)
decrease_y_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_y, 0.5)
)
self.ui.spinBox_step_y.setToolTip("Increase step size: Alt+A\nDecrease step size: Alt+Z")
# Stop Button
self.ui.pushButton_stop.setShortcut("Ctrl+X")
self.ui.pushButton_stop.setToolTip("Ctrl+X")
def _update_arrow_key_shortcuts(self) -> None:
"""Update the arrow key shortcuts based on the checkbox state."""
if self.ui.checkBox_enableArrows.isChecked():
# Set the arrow key shortcuts for motor movement
self.ui.toolButton_right.setShortcut(Qt.Key_Right)
self.ui.toolButton_left.setShortcut(Qt.Key_Left)
self.ui.toolButton_up.setShortcut(Qt.Key_Up)
self.ui.toolButton_down.setShortcut(Qt.Key_Down)
else:
# Clear the shortcuts
self.ui.toolButton_right.setShortcut("")
self.ui.toolButton_left.setShortcut("")
self.ui.toolButton_up.setShortcut("")
self.ui.toolButton_down.setShortcut("")
def _update_precision(self, precision: int) -> None:
"""
Update the precision of the coordinates.
Args:
precision(int): Precision of the coordinates.
"""
self.ui.spinBox_step_x.setDecimals(precision)
self.ui.spinBox_step_y.setDecimals(precision)
self.precision_signal.emit(precision)
def _change_step_size(self, spinBox: QDoubleSpinBox, factor: float) -> None:
"""
Change the step size of the spinbox.
Args:
spinBox(QDoubleSpinBox): Spinbox to change the step size.
factor(float): Factor to change the step size.
"""
old_step = spinBox.value()
new_step = old_step * factor
spinBox.setValue(new_step)
def _sync_step_sizes(self):
"""Sync step sizes based on checkbox state."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_x.value()
self.ui.spinBox_step_y.setValue(value)
def _update_step_size_x(self):
"""Update step size for x if checkbox is checked."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_x.value()
self.ui.spinBox_step_y.setValue(value)
def _update_step_size_y(self):
"""Update step size for y if checkbox is checked."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_y.value()
self.ui.spinBox_step_x.setValue(value)
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str):
"""
Change the active motors and update config.
Can be connected to the selected_motors_signal from MotorControlSelection.
Args:
motor_x(str): New motor X to be controlled.
motor_y(str): New motor Y to be controlled.
"""
self.motor_x = motor_x
self.motor_y = motor_y
self.config["motor_control"]["motor_x"] = motor_x
self.config["motor_control"]["motor_y"] = motor_y
@pyqtSlot(bool)
def enable_motor_controls(self, disable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
disable(bool): True to disable, False to enable.
"""
# Disable or enable all controls within the motorControl_absolute group box
for widget in self.ui.motorControl.findChildren(QWidget):
widget.setEnabled(disable)
# Enable the pushButton_stop if the motor is moving
self.ui.pushButton_stop.setEnabled(True)
def move_motor_relative(self, motor, axis: str, direction: int) -> None:
"""
Move the motor relative to the current position.
Args:
motor: Motor to move.
axis(str): Axis to move.
direction(int): Direction to move. 1 for positive, -1 for negative.
"""
if axis == "x":
step = direction * self.ui.spinBox_step_x.value()
elif axis == "y":
step = direction * self.ui.spinBox_step_y.value()
self.motor_thread.move_relative(motor, step)

View File

@@ -1,298 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>285</width>
<height>405</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>285</width>
<height>405</height>
</size>
</property>
<property name="windowTitle">
<string>Motor Control Relative</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="motorControl">
<property name="minimumSize">
<size>
<width>261</width>
<height>394</height>
</size>
</property>
<property name="title">
<string>Motor Control Relative</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="checkBox_enableArrows">
<property name="text">
<string>Move with arrow keys</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_same_xy">
<property name="text">
<string>Step [X] = Step [Y]</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="step_grid">
<item row="2" column="0">
<widget class="QLabel" name="label_step_y">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [Y]</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Decimal</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_x">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_step_x">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [X]</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_y">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_precision">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>8</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="direction_grid">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item row="1" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_up">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::UpArrow</enum>
</property>
</widget>
</item>
<item row="2" column="4">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_down">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::DownArrow</enum>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="toolButton_left">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::LeftArrow</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="toolButton_right">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::RightArrow</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="pushButton_stop">
<property name="text">
<string>Stop Movement</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,110 +0,0 @@
# pylint: disable = no-name-in-module,missing-module-docstring
import os
from qtpy import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QComboBox
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
class MotorControlSelection(MotorControlWidget):
"""
Widget for selecting the motors to control.
Signals:
selected_motors_signal (pyqtSignal(str,str)): Signal to emit the selected motors.
Slots:
get_available_motors (pyqtSlot): Slot to populate the available motors in the combo boxes and set the index based on the configuration.
enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls GUI.
on_config_update (pyqtSlot(dict)): Slot to update the config dict.
"""
selected_motors_signal = pyqtSignal(str, str)
def _load_ui(self):
"""Load the UI from the .ui file."""
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "selection.ui"), self)
def _init_ui(self):
"""Initialize the UI."""
# Lock GUI while motors are moving
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
self.pushButton_connecMotors.clicked.connect(self.select_motor)
self.get_available_motors()
# Connect change signals to change color
self.comboBox_motor_x.currentIndexChanged.connect(
lambda: self.set_combobox_style(self.comboBox_motor_x, "#ffa700")
)
self.comboBox_motor_y.currentIndexChanged.connect(
lambda: self.set_combobox_style(self.comboBox_motor_y, "#ffa700")
)
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""
Update config dict
Args:
config(dict): New config dict
"""
self.config = config
# Get motor names
self.motor_x, self.motor_y = (
self.config["motor_control"]["motor_x"],
self.config["motor_control"]["motor_y"],
)
self._init_ui()
@pyqtSlot(bool)
def enable_motor_controls(self, enable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
enable(bool): True to enable, False to disable.
"""
self.motorSelection.setEnabled(enable)
@pyqtSlot()
def get_available_motors(self) -> None:
"""
Slot to populate the available motors in the combo boxes and set the index based on the configuration.
"""
# Get all available motors
self.motor_list = self.motor_thread.get_all_motors_names()
# Populate the combo boxes
self.comboBox_motor_x.addItems(self.motor_list)
self.comboBox_motor_y.addItems(self.motor_list)
# Set the index based on the config if provided
if self.config:
index_x = self.comboBox_motor_x.findText(self.motor_x)
index_y = self.comboBox_motor_y.findText(self.motor_y)
self.comboBox_motor_x.setCurrentIndex(index_x if index_x != -1 else 0)
self.comboBox_motor_y.setCurrentIndex(index_y if index_y != -1 else 0)
def set_combobox_style(self, combobox, color: str) -> None:
"""
Set the combobox style to a specific color.
Args:
combobox(QComboBox): Combobox to change the color.
color(str): Color to set the combobox to.
"""
combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
def select_motor(self):
"""Emit the selected motors"""
motor_x = self.comboBox_motor_x.currentText()
motor_y = self.comboBox_motor_y.currentText()
# Reset the combobox color to normal after selection
self.set_combobox_style(self.comboBox_motor_x, "")
self.set_combobox_style(self.comboBox_motor_y, "")
self.selected_motors_signal.emit(motor_x, motor_y)

View File

@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>285</width>
<height>156</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>285</width>
<height>156</height>
</size>
</property>
<property name="windowTitle">
<string>Motor Control Selection</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="motorSelection">
<property name="minimumSize">
<size>
<width>261</width>
<height>145</height>
</size>
</property>
<property name="title">
<string>Motor Selection</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Motor X</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_motor_x"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Motor Y</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QPushButton" name="pushButton_connecMotors">
<property name="text">
<string>Connect Motors</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_motor_y"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,19 +1,19 @@
import os
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QLabel, QVBoxLayout, QWidget
from qtpy.QtWidgets import QVBoxLayout
from bec_widgets.qt_utils.settings_dialog import SettingWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.widget_io import WidgetIO
class MotorMapSettings(QWidget):
def __init__(self, parent=None, target_widget: QWidget = None, *args, **kwargs):
class MotorMapSettings(SettingWidget):
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, "motor_map_settings.ui"))
self.target_widget = target_widget
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
@@ -36,7 +36,7 @@ class MotorMapSettings(QWidget):
precision = WidgetIO.get_value(self.ui.precision)
scatter_size = WidgetIO.get_value(self.ui.scatter_size)
background_intensity = int(WidgetIO.get_value(self.ui.background_value) * 0.01 * 255)
color = self.ui.color.color().toTuple()
color = self.ui.color.get_color("RGBA")
if self.target_widget is not None:
self.target_widget.set_max_points(max_points)
@@ -45,29 +45,3 @@ class MotorMapSettings(QWidget):
self.target_widget.set_scatter_size(scatter_size)
self.target_widget.set_background_value(background_intensity)
self.target_widget.set_color(color)
class MotorMapDialog(QDialog):
def __init__(self, parent=None, target_widget: QWidget = None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.setModal(False)
self.setWindowTitle("Motor Map Settings")
self.target_widget = target_widget
self.widget = MotorMapSettings(target_widget=self.target_widget)
self.widget.display_current_settings(self.target_widget._config_dict)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.widget)
self.layout.addWidget(self.button_box)
@Slot()
def accept(self):
self.widget.accept_changes()
super().accept()

View File

@@ -4,8 +4,8 @@ from qtpy.QtCore import QSize
from qtpy.QtGui import QAction, QIcon
from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget
from bec_widgets.widgets.device_inputs import DeviceComboBox
from bec_widgets.widgets.toolbar.toolbar import ToolBarAction
from bec_widgets.qt_utils.toolbar import ToolBarAction
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
class DeviceSelectionAction(ToolBarAction):

View File

@@ -2,20 +2,20 @@ from __future__ import annotations
import sys
from qtpy import PYSIDE6
from qtpy.QtWidgets import QVBoxLayout, QWidget
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
from bec_widgets.qt_utils.toolbar import ModularToolBar
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.figure.plots.motor_map.motor_map import MotorMapConfig
from bec_widgets.widgets.motor_map.motor_map_dialog.motor_map_settings import MotorMapDialog
from bec_widgets.widgets.motor_map.motor_map_dialog.motor_map_settings import MotorMapSettings
from bec_widgets.widgets.motor_map.motor_map_dialog.motor_map_toolbar import (
ConnectAction,
DeviceSelectionAction,
ResetHistoryAction,
SettingsAction,
)
from bec_widgets.widgets.toolbar import ModularToolBar
class BECMotorMapWidget(BECConnector, QWidget):
@@ -37,10 +37,6 @@ class BECMotorMapWidget(BECConnector, QWidget):
client=None,
gui_id: str | None = None,
) -> None:
if not PYSIDE6:
raise RuntimeError(
"PYSIDE6 is not available in the environment. This widget is compatible only with PySide6."
)
if config is None:
config = MotorMapConfig(widget_class=self.__class__.__name__)
else:
@@ -97,7 +93,9 @@ class BECMotorMapWidget(BECConnector, QWidget):
toolbar_y.setStyleSheet("QComboBox {{ background-color: " "; }}")
def show_settings(self) -> None:
dialog = MotorMapDialog(self, target_widget=self)
dialog = SettingsDialog(
self, settings_widget=MotorMapSettings(), window_title="Motor Map Settings"
)
dialog.exec()
###################################
@@ -216,13 +214,6 @@ class BECMotorMapWidget(BECConnector, QWidget):
def main(): # pragma: no cover
if not PYSIDE6:
print(
"PYSIDE6 is not available in the environment. UI files with BEC custom widgets are runnable only with PySide6."
)
return
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)

View File

@@ -0,0 +1,71 @@
from qtpy.QtCore import Qt, Slot
from qtpy.QtGui import QPainter, QPen
from qtpy.QtWidgets import QWidget
class PositionIndicator(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.position = 0.5
self.min_value = 0
self.max_value = 100
self.scaling_factor = 0.5
self.setMinimumHeight(10)
def set_range(self, min_value, max_value):
self.min_value = min_value
self.max_value = max_value
@Slot(float)
def on_position_update(self, position: float):
self.position = position
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
width = self.width()
height = self.height()
# Draw horizontal line
painter.setPen(Qt.black)
painter.drawLine(0, height // 2, width, height // 2)
# Draw shorter vertical line at the current position
x_pos = int(self.position * width)
painter.setPen(QPen(Qt.red, 2))
short_line_height = int(height * self.scaling_factor)
painter.drawLine(
x_pos,
(height // 2) - (short_line_height // 2),
x_pos,
(height // 2) + (short_line_height // 2),
)
# Draw thicker vertical lines at the ends
end_line_pen = QPen(Qt.blue, 5)
painter.setPen(end_line_pen)
painter.drawLine(0, 0, 0, height)
painter.drawLine(width - 1, 0, width - 1, height)
if __name__ == "__main__":
from qtpy.QtWidgets import QApplication, QSlider, QVBoxLayout
app = QApplication([])
position_indicator = PositionIndicator()
slider = QSlider(Qt.Horizontal)
slider.valueChanged.connect(lambda value: position_indicator.on_position_update(value / 100))
layout = QVBoxLayout()
layout.addWidget(position_indicator)
layout.addWidget(slider)
widget = QWidget()
widget.setLayout(layout)
widget.show()
app.exec_()

View File

@@ -0,0 +1 @@
{'files': ['position_indicator.py']}

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.position_indicator.position_indicator import PositionIndicator
DOM_XML = """
<ui language='c++'>
<widget class='PositionIndicator' name='position_indicator'>
</widget>
</ui>
"""
class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = PositionIndicator(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "position_indicator"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "PositionIndicator"
def toolTip(self):
return "PositionIndicator"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,17 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.position_indicator.position_indicator_plugin import (
PositionIndicatorPlugin,
)
QPyDesignerCustomWidgetCollection.addCustomWidget(PositionIndicatorPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -12,8 +12,8 @@ from qtpy.QtWidgets import (
)
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.buttons.stop_button.stop_button import StopButton
from bec_widgets.widgets.scan_control.scan_group_box import ScanGroupBox
from bec_widgets.widgets.stop_button.stop_button import StopButton
class ScanControl(BECConnector, QWidget):

View File

@@ -12,7 +12,7 @@ from qtpy.QtWidgets import (
)
from bec_widgets.utils.widget_io import WidgetIO
from bec_widgets.widgets.device_inputs import DeviceLineEdit
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
class ScanArgType:

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.spinner.spinner_widget_plugin import SpinnerWidgetPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(SpinnerWidgetPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,85 @@
import sys
import numpy as np
import qdarktheme
from qtpy.QtCore import QRect, Qt, QTimer
from qtpy.QtGui import QColor, QPainter, QPen
from qtpy.QtWidgets import QApplication, QMainWindow, QWidget
def ease_in_out_sine(t):
return 1 - np.sin(np.pi * t)
class SpinnerWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.angle = 0
self.timer = QTimer(self)
self.timer.timeout.connect(self.rotate)
self.time = 0
self.duration = 50
self.speed = 50
self._started = False
def start(self):
if self._started:
return
self.timer.start(self.speed)
self._started = True
def stop(self):
if not self._started:
return
self.timer.stop()
self._started = False
self.update()
def rotate(self):
self.time = (self.time + 1) % self.duration
t = self.time / self.duration
easing_value = ease_in_out_sine(t)
self.angle -= (20 * easing_value) % 360 + 10
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
size = min(self.width(), self.height())
rect = QRect(0, 0, size, size)
background_color = QColor(200, 200, 200, 50)
line_width = 5
color_palette = qdarktheme.load_palette()
color = QColor(color_palette.accent().color())
rect.adjust(line_width, line_width, -line_width, -line_width)
# Background arc
painter.setPen(QPen(background_color, line_width, Qt.SolidLine))
adjusted_rect = QRect(rect.left(), rect.top(), rect.width(), rect.height())
painter.drawArc(adjusted_rect, 0, 360 * 16)
if self._started:
# Foreground arc
pen = QPen(color, line_width, Qt.SolidLine)
pen.setCapStyle(Qt.RoundCap)
painter.setPen(pen)
proportion = 1 / 4
angle_span = int(proportion * 360 * 16)
angle_span += angle_span * ease_in_out_sine(self.time / self.duration)
painter.drawArc(adjusted_rect, self.angle * 16, int(angle_span))
painter.end()
if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
window = QMainWindow()
widget = SpinnerWidget()
widget.start()
window.setCentralWidget(widget)
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1 @@
{'files': ['spinner.py']}

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
DOM_XML = """
<ui language='c++'>
<widget class='SpinnerWidget' name='spinner_widget'>
</widget>
</ui>
"""
class SpinnerWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = SpinnerWidget(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "spinner_widget"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "SpinnerWidget"
def toolTip(self):
return "SpinnerWidget"
def whatsThis(self):
return self.toolTip()

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.stop_button.stop_button_plugin import StopButtonPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(StopButtonPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -1,3 +1,4 @@
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QPushButton
from bec_widgets.utils import BECConnector
@@ -12,21 +13,13 @@ class StopButton(BECConnector, QPushButton):
self.get_bec_shortcuts()
self.setText("Stop")
self.setStyleSheet("background-color: #cc181e; color: white")
self.setStyleSheet(
"background-color: #cc181e; color: white; font-weight: bold; font-size: 12px;"
)
self.clicked.connect(self.stop_scan)
@Slot()
def stop_scan(self):
"""Stop the scan."""
self.queue.request_scan_abortion()
self.queue.request_queue_reset()
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = StopButton()
widget.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1 @@
{'files': ['stop_button.py']}

View File

@@ -0,0 +1,57 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.stop_button.stop_button import StopButton
DOM_XML = """
<ui language='c++'>
<widget class='StopButton' name='stop_button'>
</widget>
</ui>
"""
class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = StopButton(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "BEC Buttons"
def icon(self):
current_path = os.path.dirname(__file__)
icon_path = os.path.join(current_path, "assets", "stop.png")
return QIcon(icon_path)
def includeFile(self):
return "stop_button"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "StopButton"
def toolTip(self):
return "A button that stops the current scan."
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.toggle.toggle_switch_plugin import ToggleSwitchPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(ToggleSwitchPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,149 @@
import sys
from qtpy.QtCore import Property, QEasingCurve, QPointF, QPropertyAnimation, Qt
from qtpy.QtGui import QColor, QPainter
from qtpy.QtWidgets import QApplication, QWidget
class ToggleSwitch(QWidget):
"""
A simple toggle.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(40, 21)
self._thumb_pos = QPointF(3, 2) # Use QPointF for the thumb position
self._active_track_color = QColor(33, 150, 243)
self._active_thumb_color = QColor(255, 255, 255)
self._inactive_track_color = QColor(200, 200, 200)
self._inactive_thumb_color = QColor(255, 255, 255)
self._checked = False
self._track_color = self.inactive_track_color
self._thumb_color = self.inactive_thumb_color
self._animation = QPropertyAnimation(self, b"thumb_pos")
self._animation.setDuration(200)
self._animation.setEasingCurve(QEasingCurve.Type.OutBack)
self.setProperty("checked", True)
@Property(bool)
def checked(self):
"""
The checked state of the toggle switch.
"""
return self._checked
@checked.setter
def checked(self, state):
self._checked = state
self.update_colors()
self.set_thumb_pos_to_state()
@Property(QPointF)
def thumb_pos(self):
return self._thumb_pos
@thumb_pos.setter
def thumb_pos(self, pos):
self._thumb_pos = pos
self.update()
@Property(QColor)
def active_track_color(self):
return self._active_track_color
@active_track_color.setter
def active_track_color(self, color):
self._active_track_color = color
self.update_colors()
self.update()
@Property(QColor)
def active_thumb_color(self):
return self._active_thumb_color
@active_thumb_color.setter
def active_thumb_color(self, color):
self._active_thumb_color = color
self.update_colors()
self.update()
@Property(QColor)
def inactive_track_color(self):
return self._inactive_track_color
@inactive_track_color.setter
def inactive_track_color(self, color):
self._inactive_track_color = color
self.update_colors()
self.update()
@Property(QColor)
def inactive_thumb_color(self):
return self._inactive_thumb_color
@inactive_thumb_color.setter
def inactive_thumb_color(self, color):
self._inactive_thumb_color = color
self.update_colors()
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Draw track
painter.setBrush(self._track_color)
painter.setPen(Qt.NoPen)
painter.drawRoundedRect(
0, 0, self.width(), self.height(), self.height() / 2, self.height() / 2
)
# Draw thumb
painter.setBrush(self._thumb_color)
diameter = int(self.height() * 0.8)
painter.drawEllipse(int(self._thumb_pos.x()), int(self._thumb_pos.y()), diameter, diameter)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self._checked = not self._checked
self.update_colors()
self.animate_thumb()
def update_colors(self):
self._thumb_color = self.active_thumb_color if self._checked else self.inactive_thumb_color
self._track_color = self.active_track_color if self._checked else self.inactive_track_color
def get_thumb_pos(self, checked):
return QPointF(self.width() - self.height() + 3, 2) if checked else QPointF(3, 2)
def set_thumb_pos_to_state(self):
# this is to avoid that linter complains about the thumb_pos setter
self.setProperty("thumb_pos", self.get_thumb_pos(self._checked))
self.update_colors()
def animate_thumb(self):
start_pos = self.thumb_pos
end_pos = self.get_thumb_pos(self._checked)
self._animation.stop()
self._animation.setStartValue(start_pos)
self._animation.setEndValue(end_pos)
self._animation.start()
def sizeHint(self):
return self.minimumSizeHint()
def minimumSizeHint(self):
return self.size()
if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
window = ToggleSwitch()
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1 @@
{'files': ['toggle.py']}

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.toggle.toggle import ToggleSwitch
DOM_XML = """
<ui language='c++'>
<widget class='ToggleSwitch' name='toggle_switch'>
</widget>
</ui>
"""
class ToggleSwitchPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = ToggleSwitch(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "toggle_switch"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "ToggleSwitch"
def toolTip(self):
return "ToggleSwitch"
def whatsThis(self):
return self.toolTip()

View File

@@ -1 +0,0 @@
from .toolbar import ModularToolBar

View File

@@ -49,13 +49,6 @@ class VSCodeEditor(WebsiteWidget):
break
self.set_url(self._url)
def closeEvent(self, event):
"""
Hook for the close event to terminate the server.
"""
self.cleanup_vscode()
super().closeEvent(event)
def cleanup_vscode(self):
"""
Cleanup the VSCode editor.
@@ -72,13 +65,6 @@ class VSCodeEditor(WebsiteWidget):
self.cleanup_vscode()
return super().cleanup()
def close(self):
"""
Close the widget.
"""
self.cleanup_vscode()
return super().close()
if __name__ == "__main__": # pragma: no cover
import sys

View File

@@ -69,6 +69,6 @@ if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
mainWin = WebsiteWidget("https://scilog.psi.ch")
mainWin = WebsiteWidget(url="https://scilog.psi.ch")
mainWin.show()
sys.exit(app.exec())

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "0.81.0"
version = "0.82.1"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -49,11 +49,19 @@ class FakePositioner(FakeDevice):
self.read_value = read_value
self.name = name
@property
def precision(self):
return 3
def set_read_value(self, value):
self.read_value = value
def read(self):
return {self.name: {"value": self.read_value}}
return {
self.name: {"value": self.read_value},
f"{self.name}_setpoint": {"value": self.read_value},
f"{self.name}_motor_is_moving": {"value": 0},
}
def set_limits(self, limits):
self.limits = limits
@@ -72,6 +80,13 @@ class FakePositioner(FakeDevice):
return MagicMock(get=MagicMock(return_value=self.read_value))
class Positioner(FakePositioner):
"""just placeholder for testing embeded isinstance check in DeviceCombobox"""
def __init__(self, name="test", limits=None, read_value=1.0):
super().__init__(name, limits, read_value)
class DMMock:
def __init__(self):
self.devices = DeviceContainer()
@@ -95,6 +110,7 @@ DEVICES = [
FakeDevice("bpm3a"),
FakeDevice("bpm3i"),
FakeDevice("eiger"),
Positioner("test", limits=[-10, 10], read_value=2.0),
]

View File

@@ -0,0 +1,98 @@
from unittest import mock
import pytest
from bec_lib.endpoints import MessageEndpoints
from bec_lib.messages import ScanQueueMessage
from qtpy.QtGui import QValidator
from bec_widgets.widgets.device_box.device_box import DeviceBox
from .client_mocks import mocked_client
@pytest.fixture
def device_box(qtbot, mocked_client):
with mock.patch("bec_widgets.widgets.device_box.device_box.uuid.uuid4") as mock_uuid:
mock_uuid.return_value = "fake_uuid"
db = DeviceBox(device="samx", client=mocked_client)
qtbot.addWidget(db)
yield db
def test_device_box(device_box):
assert device_box.device == "samx"
data = device_box.dev["samx"].read()
setpoint_text = device_box.ui.setpoint.text()
# check that the setpoint is taken correctly after init
assert float(setpoint_text) == data["samx_setpoint"]["value"]
# check that the precision is taken correctly after init
precision = device_box.dev["samx"].precision
assert setpoint_text == f"{data['samx_setpoint']['value']:.{precision}f}"
# check that the step size is set according to the device precision
assert device_box.ui.step_size.value() == 10**-precision * 10
def test_device_box_update_limits(device_box):
device_box._limits = None
device_box.update_limits([0, 10])
assert device_box._limits == [0, 10]
assert device_box.setpoint_validator.bottom() == 0
assert device_box.setpoint_validator.top() == 10
assert device_box.setpoint_validator.validate("100", 0) == (
QValidator.State.Intermediate,
"100",
0,
)
device_box.update_limits(None)
assert device_box._limits is None
assert device_box.setpoint_validator.validate("100", 0) == (
QValidator.State.Acceptable,
"100",
0,
)
def test_device_box_on_stop(device_box):
with mock.patch.object(device_box.client.connector, "send") as mock_send:
device_box.on_stop()
params = {"device": "samx", "rpc_id": "fake_uuid", "func": "stop", "args": [], "kwargs": {}}
msg = ScanQueueMessage(
scan_type="device_rpc",
parameter=params,
queue="emergency",
metadata={"RID": "fake_uuid", "response": False},
)
mock_send.assert_called_once_with(MessageEndpoints.scan_queue_request(), msg)
def test_device_box_setpoint_change(device_box):
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
device_box.ui.setpoint.setText("100")
device_box.on_setpoint_change()
mock_move.assert_called_once_with(100, relative=False)
def test_device_box_on_tweak_right(device_box):
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
device_box.ui.step_size.setValue(0.1)
device_box.on_tweak_right()
mock_move.assert_called_once_with(0.1, relative=True)
def test_device_box_on_tweak_left(device_box):
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
device_box.ui.step_size.setValue(0.1)
device_box.on_tweak_left()
mock_move.assert_called_once_with(-0.1, relative=True)
def test_device_box_setpoint_out_of_range(device_box):
device_box.update_limits([0, 10])
device_box.ui.setpoint.setText("100")
device_box.on_setpoint_change()
assert device_box.ui.setpoint.text() == "100"
assert device_box.ui.setpoint.hasAcceptableInput() == False

View File

@@ -1,6 +1,6 @@
import pytest
from bec_widgets.widgets.device_inputs.device_input_base import DeviceInputBase
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase
from .client_mocks import mocked_client
@@ -63,4 +63,4 @@ def test_device_input_base_get_device_list(device_input_base):
def test_device_input_base_get_filters(device_input_base):
filters = device_input_base.get_available_filters()
assert filters == {"FakePositioner", "FakeDevice"}
assert filters == {"FakePositioner", "FakeDevice", "Positioner"}

View File

@@ -1,7 +1,7 @@
import pytest
from bec_widgets.widgets.device_inputs.device_combobox.device_combobox import DeviceComboBox
from bec_widgets.widgets.device_inputs.device_line_edit.device_line_edit import DeviceLineEdit
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
from .client_mocks import mocked_client
@@ -67,6 +67,7 @@ def test_device_input_combobox_init(device_input_combobox):
"bpm3a",
"bpm3i",
"eiger",
"test",
]
@@ -153,6 +154,7 @@ def test_device_input_line_edit_init(device_input_line_edit):
"bpm3a",
"bpm3i",
"eiger",
"test",
]

Some files were not shown because too many files have changed in this diff Show More