1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-09 18:20:55 +02:00

Compare commits

...

40 Commits

Author SHA1 Message Date
semantic-release
49a5a23d41 0.94.5
Automatically generated by python-semantic-release
2024-08-14 12:01:45 +00:00
4f96d0e4a1 build: increased min version of bec to 2.21.4
Since we now rely on reusing the BECClient singleton, we need the fix introduced with 2.21.4 in BEC.
2024-08-14 12:32:34 +02:00
ea9240d2f7 fix(rpc): use client singleton instead of dispatcher 2024-08-14 12:32:34 +02:00
4d02b42f11 fix: removed qcoreapplication for polling events 2024-08-14 12:32:34 +02:00
semantic-release
9509be14be 0.94.4
Automatically generated by python-semantic-release
2024-08-14 08:46:53 +00:00
198c1d1064 fix: do not shutdown client in "close"
Terminating client connections has to be done at the application level
2024-08-13 12:23:51 +02:00
2af5c94913 docs: review developer section; add introduction 2024-08-13 11:06:24 +02:00
semantic-release
a4a0bac3c1 0.94.3
Automatically generated by python-semantic-release
2024-08-13 09:03:11 +00:00
f285b35b49 test(waveform_widget): added tests for axis setting and curve dialog 2024-08-13 10:53:44 +02:00
7aeb2b5c26 fix(curve_dialog): async curves are shown in curve dialog after addition. 2024-08-13 10:53:44 +02:00
d56ea95ef9 fix(waveform): async device entry is correctly passed, updated and with new scan the previous data are cleared 2024-08-13 10:53:44 +02:00
semantic-release
5733fea98c 0.94.2
Automatically generated by python-semantic-release
2024-08-13 08:53:10 +00:00
98b79aac7b fix(image): image is single image mode do not raise popup error when connected twice with the same monitor 2024-08-12 11:24:08 +02:00
semantic-release
4212fe0e32 0.94.1
Automatically generated by python-semantic-release
2024-08-12 08:53:48 +00:00
93d397759c fix: issue #292, wrong key was used to clean _slots internal dictionary 2024-08-12 10:32:35 +02:00
semantic-release
8c5b901a37 0.94.0
Automatically generated by python-semantic-release
2024-08-08 14:59:14 +00:00
0273bf4856 refactor: adjust dimensions 2024-08-08 15:11:43 +02:00
c80a7cd108 feat: add PositionerControlLine 2024-08-08 14:58:16 +02:00
semantic-release
a50d9c7b3f 0.93.5
Automatically generated by python-semantic-release
2024-08-08 11:48:07 +00:00
281633deff fix(positioner_box): icons fixed 2024-08-08 13:34:07 +02:00
0d190c5c59 refactor: add button for positioner selection 2024-08-08 13:34:07 +02:00
6269009e54 test(dap): wait for fit 2024-08-07 20:19:37 +02:00
6d2442d23c test(auto-update): wait for rendering 2024-08-07 19:58:26 +02:00
semantic-release
110b27351b 0.93.4
Automatically generated by python-semantic-release
2024-08-07 16:21:43 +00:00
37aa371e7c fix: rename DeviceBox to PositionerBox, fix test for validation 2024-08-07 17:56:48 +02:00
eb54e9f788 fix: add validation for bec_lib.device.Positioner; closes #268 2024-08-07 15:45:39 +02:00
semantic-release
482efeb340 0.93.3
Automatically generated by python-semantic-release
2024-08-07 13:14:54 +00:00
99ee545e41 fix(dock): properly shut down docks and temp areas 2024-08-07 13:58:43 +02:00
cf94599c25 test: removed quit from teardown 2024-08-07 12:25:54 +02:00
b50b3a27e6 fix(settings): shut down settings dialog 2024-08-07 12:25:54 +02:00
bf6294ecbf test: removed explicit call to close the widget 2024-08-07 12:25:54 +02:00
a3d4f5ac4b fix(website): fixed teardown of website widgets 2024-08-07 11:15:14 +02:00
bc264975b1 fix(dock): properly shut down docks and dock areas 2024-08-07 11:00:25 +02:00
ad07bbf85e fix(figure): cleanup pyqtgraph 2024-08-07 10:12:49 +02:00
9856857f4c test: use factory instead of fixture to properly cleanup widgets on teardown 2024-08-07 10:12:49 +02:00
f9e5897900 test: ensure all toplevelwidgets are closed 2024-08-07 10:12:49 +02:00
semantic-release
39fb22b716 0.93.2
Automatically generated by python-semantic-release
2024-08-07 07:57:18 +00:00
a372925fff fix(scan_group_box): Scan Spinboxes limits increased to max allowed values; setting dialog for step size and decimal precision for ScanDoubleSpinBox on right click 2024-08-07 09:47:06 +02:00
semantic-release
ec54440569 0.93.1
Automatically generated by python-semantic-release
2024-08-06 21:56:54 +00:00
af86860bf3 fix(dock): docks have more recognizable red icon for closing docks 2024-08-06 19:23:31 +02:00
73 changed files with 1541 additions and 547 deletions

View File

@@ -1,5 +1,129 @@
# CHANGELOG
## v0.94.5 (2024-08-14)
### Build
* build: increased min version of bec to 2.21.4
Since we now rely on reusing the BECClient singleton, we need the fix introduced with 2.21.4 in BEC. ([`4f96d0e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f96d0e4a14edc4b2839c1dddeda384737dc7a8a))
### Fix
* fix(rpc): use client singleton instead of dispatcher ([`ea9240d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ea9240d2f71931082f33fb6b68231469875c3d63))
* fix: removed qcoreapplication for polling events ([`4d02b42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4d02b42f11e9882b843317255a4975565c8a536f))
## v0.94.4 (2024-08-14)
### Documentation
* docs: review developer section; add introduction ([`2af5c94`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2af5c94913a3435c1839034df4f45f885b56d08b))
### Fix
* fix: do not shutdown client in "close"
Terminating client connections has to be done at the application level ([`198c1d1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/198c1d1064cc2dae55de4b941929341faddacb28))
## v0.94.3 (2024-08-13)
### Fix
* fix(curve_dialog): async curves are shown in curve dialog after addition. ([`7aeb2b5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7aeb2b5c26c7c2851e8d663d32521da8daec95ef))
* fix(waveform): async device entry is correctly passed, updated and with new scan the previous data are cleared ([`d56ea95`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d56ea95ef97bfdd0bc3eeddc4505d20b38e28559))
### Test
* test(waveform_widget): added tests for axis setting and curve dialog ([`f285b35`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f285b35b491660549e74349318119f7c2c44f619))
## v0.94.2 (2024-08-13)
### Fix
* fix(image): image is single image mode do not raise popup error when connected twice with the same monitor ([`98b79aa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/98b79aac7b47b73137f4d582f7f1d552b1d95366))
## v0.94.1 (2024-08-12)
### Fix
* fix: issue #292, wrong key was used to clean _slots internal dictionary ([`93d3977`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/93d397759c756397604ebff5e24f3a580be8620d))
## v0.94.0 (2024-08-08)
### Feature
* feat: add PositionerControlLine ([`c80a7cd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c80a7cd1083baa9543a2cee2e3c3a51dfd209b19))
### Refactor
* refactor: adjust dimensions ([`0273bf4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0273bf485694609325b5b556a3c69fb53c18446e))
## v0.93.5 (2024-08-08)
### Fix
* fix(positioner_box): icons fixed ([`281633d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/281633deff15b6879dac3a4f0770fa6949aaecdc))
### Refactor
* refactor: add button for positioner selection ([`0d190c5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0d190c5c5996e59fec4bdd44d2003e10e200b009))
### Test
* test(dap): wait for fit ([`6269009`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6269009e5451f830cdee58a514c7858483488a8d))
* test(auto-update): wait for rendering ([`6d2442d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6d2442d23c683fe92af13df982ce681c07e99cde))
## v0.93.4 (2024-08-07)
### Fix
* fix: rename DeviceBox to PositionerBox, fix test for validation ([`37aa371`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37aa371e7c4c62d70abf37abc125db0c088790fe))
* fix: add validation for bec_lib.device.Positioner; closes #268 ([`eb54e9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb54e9f788e97af23db8fe0c78f8facb8688bb99))
## v0.93.3 (2024-08-07)
### Fix
* fix(dock): properly shut down docks and temp areas ([`99ee545`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99ee545e41c6078654958b668b5b329f85553d16))
* fix(settings): shut down settings dialog ([`b50b3a2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b50b3a27e68956e10e8169a0aa698c911d2d9642))
* fix(website): fixed teardown of website widgets ([`a3d4f5a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3d4f5ac4bc52acfed2791a1724fade6972ed320))
* fix(dock): properly shut down docks and dock areas ([`bc26497`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bc264975b1363c9dfea516621d7878c320677d15))
* fix(figure): cleanup pyqtgraph ([`ad07bbf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ad07bbf85e9c8d9838bdd686f69d41c235b7db19))
### Test
* test: removed quit from teardown ([`cf94599`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf94599c2544d6831c8afbe7b340082077557ed1))
* test: removed explicit call to close the widget ([`bf6294e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bf6294ecbfd494565d2dc215e4d7e0c280ac7745))
* test: use factory instead of fixture to properly cleanup widgets on teardown ([`9856857`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9856857f4cc7fa229c10d00fbae4452464a207cb))
* test: ensure all toplevelwidgets are closed ([`f9e5897`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f9e58979009cf632feea529700ad191401dd7eb8))
## v0.93.2 (2024-08-07)
### Fix
* fix(scan_group_box): Scan Spinboxes limits increased to max allowed values; setting dialog for step size and decimal precision for ScanDoubleSpinBox on right click ([`a372925`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a372925fffa787c686198ae7cb3f9c15b459c109))
## v0.93.1 (2024-08-06)
### Documentation
* docs: added video tutorial section with BSEG YT video ([`302ae90`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/302ae90139f6a88e2401fe29fe312387486e27a9))
### Fix
* fix(dock): docks have more recognizable red icon for closing docks ([`af86860`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af86860bf35474805fb1a7bc3725cf8835ed4cc7))
## v0.93.0 (2024-08-05)
### Feature
@@ -31,121 +155,3 @@ This reverts commit fd6ae91993a23a7b8dbb2cf3c4b7c3eda6d2b0f6 ([`5aad401`](https:
* fix: fix missmatch of signal/slot in image and motormap ([`dcc5fd7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dcc5fd71ee9f51767a7b2b1ed6200e89d1ef754c))
## v0.92.3 (2024-07-28)
### Fix
* fix(docs): moved to pyside6 ([`71873dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/71873ddf359516ded8f74f4d2f73df4156aa1368))
## v0.92.2 (2024-07-28)
### Fix
* fix(widgets): fixed import for tictactoe example ([`995a795`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/995a795060bebe25c17108d80ae0fa30463f03b1))
## v0.92.1 (2024-07-28)
### Build
* build(ci): install ophyd_devices in editable mode for pipelines ([`06205e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/06205e07903d93accf40abab153f440059f236ed))
### Fix
* fix: use SafeSlot instead of Slot ([`bc1e239`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bc1e23944cc0e5a861e3d0b4dc5b4ac6292d5269))
* fix: linting ([`a3fe205`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3fe20500ae2ac03dcde07432f7e21ce5262ce46))
* fix: always add a QApplication for tests ([`61a4e32`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61a4e32deb337ed27f2f43358b88b7266413b58e))
* fix: add xvfb to draw offscreen ([`3d681f7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3d681f77e144e74138fc5fa65630004d7c166878))
* fix: reset ErrorPopup singleton between tests ([`5a9ccfd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5a9ccfd1f6d2aacd5d86c1a34f74163b272d1ae4))
* fix: metaclass + QObject segfaults PyQt(cpp bindings) ([`fc57b7a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc57b7a1262031a2df9e6a99493db87e766b779a))
### Refactor
* refactor: renamed DeviceMonitor2DMessage ([`4be6fd6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4be6fd6b83ea1048f16310f7d2bbe777b13b245e))
* refactor: rename device_monitor to device_monitor_2d ([`714e1e1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/714e1e139e0033d2725fefb636c419ca137a68c6))
## v0.92.0 (2024-07-24)
### Feature
* feat(dock): dock style sheets updated ([`8ca60d5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ca60d54b3cfa621172ce097fc1ba514c47ebac7))
* feat(general_gui): general gui added ([`5696c99`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5696c993dc1c0da40ff3e99f754c246cc017ea32))
### Fix
* fix(dock): custom label can be created closable ([`4457ef2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4457ef2147e21b856c9dcaf63c81ba98002dcaf1))
* fix(device_combobox): set minimum size to 125px ([`1206e15`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1206e153094cd8505badf69a1461572a76b4c5ad))
## v0.91.0 (2024-07-23)
### Feature
* feat(dock_area): plugin added ([`a16b87a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a16b87ac28d164230dd2e8020f50ff3a63cd407e))
* feat(dock_area): Added toolbar to dock area to add widgets without CLI interactions ([`cce1367`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cce1367a72fca7206d351894bd1831b7bbfa7ec6))
* feat(toolbar): expandable menu actions ([`28f26e9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/28f26e92a46063db1a194be552156a5d3b2c43e7))
### Fix
* fix(status_item): icons changed to material design ([`1b9c55a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b9c55a46a0dfd8678c8e95ff64dd6e8cfb9233e))
* fix(plugins): Qt Designer plugins icons adjusted ([`f4844d2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f4844d2e067ce75dc64b89b230d7932b308ddfc2))
### Test
* test(dock_area): tests extended ([`06fab0e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/06fab0eab926cef5677d4988fd1fce09da342dd8))
## v0.90.0 (2024-07-23)
### Feature
* feat(image_widget): plugin added ([`4371168`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/43711680ba253f81fb0ffe764bcaae701b02bb49))
* feat(image_widget): all toolbar actions added ([`501eb92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/501eb923f12fa6aaa93f5428ca78e57694edfbc0))
* feat(image_widget): image_widget added ([`6a9317f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6a9317facda896ee784c7fc1db0cd3d68cdfcf73))
### Fix
* fix(axis_setting): fix compatibility for issue with horizontal line for PyQt6 ([`1cf6e32`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1cf6e32303f82bc7c3f3391d0e96a88bc31f29fc))
* fix(image_widget): image_widget autorange fixed ([`7f49893`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7f49893d2ce3b9d02efa764f7f10442ed6ab8f3c))
* fix(image_widget): image widget adjusted ([`3d2ca48`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3d2ca4855c36fe0af59a4b540caa3c8023a81773))
* fix(image): only single monitor image is allowed ([`fe7e542`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fe7e542b19dc5b401523501acb74ac03edf62ad4))
* fix(image): raw data are saved in image item to always have precise processing ([`c15035b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c15035b6b769a96780a16da9e7f75af3b823654c))
### Refactor
* refactor(jupyter_console_example): added examples of standalone widgets ([`ba0d1ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ba0d1ea9031b4ae2e2e73bf269fbfad973b924a5))
### Test
* test(image_widget): tests added ([`70fb276`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/70fb276fdf31dffc105435d3dfe7c5caea0b10ce))
## v0.89.0 (2024-07-22)
### Feature
* feat(themes): moved themes to bec_qthemes ([`3798714`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3798714369adf4023f833b7749d2f46a0ec74eee))
### Unknown
* Revert "feat(themes): moved themes to bec_qthemes"
This reverts commit 3798714369adf4023f833b7749d2f46a0ec74eee ([`fd6ae91`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fd6ae91993a23a7b8dbb2cf3c4b7c3eda6d2b0f6))
## v0.88.1 (2024-07-22)
### Refactor
* refactor(toolbar): generalizations of the ToolBarAction ([`ad112d1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ad112d1f08157f6987edd48a0bacf9f669ef1997))

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M180-404.87v-50.26h289.75v50.26H180Zm0-162.57v-50.25h454.62v50.25H180Zm0-162.3V-780h454.62v50.26H180ZM524.62-180v-105.69l217.15-216.16q7.46-7.07 16.11-10.3 8.65-3.23 17.3-3.23 9.43 0 18.25 3.53 8.82 3.54 16.03 10.62l37 37.38q6.87 7.47 10.21 16.16Q860-439 860-430.31t-3.37 17.69q-3.37 9-10.52 16.46L630.31-180H524.62Zm250.69-211.69 37-38.62-37-37.38-38 38 38 38Z"/>
</svg>

After

Width:  |  Height:  |  Size: 492 B

View File

Before

Width:  |  Height:  |  Size: 266 B

After

Width:  |  Height:  |  Size: 266 B

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import threading
from queue import Queue
from typing import TYPE_CHECKING
from pydantic import BaseModel
@@ -25,6 +27,17 @@ class AutoUpdates:
def __init__(self, gui: BECDockArea):
self.gui = gui
self.msg_queue = Queue()
self.auto_update_thread = None
self._shutdown_sentinel = object()
self.start()
def start(self):
"""
Start the auto update thread.
"""
self.auto_update_thread = threading.Thread(target=self.process_queue)
self.auto_update_thread.start()
def start_default_dock(self):
"""
@@ -79,6 +92,16 @@ class AutoUpdates:
info = self.get_scan_info(msg)
self.handler(info)
def process_queue(self):
"""
Process the message queue.
"""
while True:
msg = self.msg_queue.get()
if msg is self._shutdown_sentinel:
break
self.run(msg)
@staticmethod
def get_selected_device(monitored_devices, selected_device):
"""
@@ -151,3 +174,11 @@ class AutoUpdates:
fig.clear_all()
plt = fig.plot(x_name=dev_x, y_name=dev_y, label=f"Scan {info.scan_number} - {dev_y}")
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
def shutdown(self):
"""
Shutdown the auto update thread.
"""
self.msg_queue.put(self._shutdown_sentinel)
if self.auto_update_thread:
self.auto_update_thread.join()

View File

@@ -21,9 +21,10 @@ class Widgets(str, enum.Enum):
BECQueue = "BECQueue"
BECStatusBox = "BECStatusBox"
BECWaveformWidget = "BECWaveformWidget"
DeviceBox = "DeviceBox"
DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit"
PositionerBox = "PositionerBox"
PositionerControlLine = "PositionerControlLine"
RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl"
StopButton = "StopButton"
@@ -2287,24 +2288,6 @@ class BECWaveformWidget(RPCBase):
"""
class DeviceBox(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
class DeviceComboBox(RPCBase):
@property
@rpc_call
@@ -2359,6 +2342,28 @@ class DeviceLineEdit(RPCBase):
"""
class PositionerBox(RPCBase):
@rpc_call
def set_positioner(self, positioner: str):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class PositionerControlLine(RPCBase):
@rpc_call
def set_positioner(self, positioner: str):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class Ring(RPCBase):
@rpc_call
def _get_all_rpc(self) -> "dict":

View File

@@ -6,17 +6,16 @@ import json
import os
import select
import subprocess
import sys
import threading
import time
import uuid
from functools import wraps
from typing import TYPE_CHECKING
from bec_lib.client import BECClient
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
from qtpy.QtCore import QEventLoop, QSocketNotifier, QTimer
import bec_widgets.cli.client as client
from bec_widgets.cli.auto_updates import AutoUpdates
@@ -24,10 +23,6 @@ from bec_widgets.cli.auto_updates import AutoUpdates
if TYPE_CHECKING:
from bec_lib.device import DeviceBase
from bec_widgets.cli.client import BECDockArea, BECFigure
from bec_lib.serialization import MsgpackSerialization
messages = lazy_import("bec_lib.messages")
# from bec_lib.connector import MessageObject
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
@@ -184,7 +179,7 @@ class BECGuiClientMixin:
if isinstance(msg, messages.ScanStatusMessage):
if not self.gui_is_alive():
return
self.auto_updates.run(msg)
self.auto_updates.msg_queue.put(msg)
def show(self) -> None:
"""
@@ -213,6 +208,8 @@ class BECGuiClientMixin:
self._process_output_processing_thread.join()
self._process.wait()
self._process = None
if self.auto_updates is not None:
self.auto_updates.shutdown()
class RPCResponseTimeoutError(Exception):
@@ -224,54 +221,14 @@ class RPCResponseTimeoutError(Exception):
)
class QtRedisMessageWaiter:
def __init__(self, redis_connector, message_to_wait):
self.ev_loop = QEventLoop()
self.response = None
self.connector = redis_connector
self.message_to_wait = message_to_wait
self.pubsub = redis_connector._redis_conn.pubsub()
self.pubsub.subscribe(self.message_to_wait.endpoint)
fd = self.pubsub.connection._sock.fileno()
self.notifier = QSocketNotifier(fd, QSocketNotifier.Read)
self.notifier.activated.connect(self._pubsub_readable)
def _msg_received(self, msg_obj):
self.response = msg_obj.value
self.ev_loop.quit()
def wait(self, timeout=1):
timer = QTimer()
timer.singleShot(timeout * 1000, self.ev_loop.quit)
self.ev_loop.exec_()
timer.stop()
self.notifier.setEnabled(False)
self.pubsub.close()
return self.response
def _pubsub_readable(self, fd):
while True:
msg = self.pubsub.get_message()
if msg:
if msg["type"] == "subscribe":
# get_message buffers, so we may already have the answer
# let's check...
continue
else:
break
else:
return
channel = msg["channel"].decode()
msg = MessageObject(topic=channel, value=MsgpackSerialization.loads(msg["data"]))
self.connector._execute_callback(self._msg_received, msg, {})
class RPCBase:
def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
self._client = BECDispatcher().client
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
self._config = config if config is not None else {}
self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())
self._parent = parent
self._msg_wait_event = threading.Event()
self._rpc_response = None
super().__init__()
# print(f"RPCBase: {self._gui_id}")
@@ -315,24 +272,39 @@ class RPCBase:
# pylint: disable=protected-access
receiver = self._root._gui_id
if wait_for_rpc_response:
redis_msg = QtRedisMessageWaiter(
self._client.connector, MessageEndpoints.gui_instruction_response(request_id)
self._rpc_response = None
self._msg_wait_event.clear()
self._client.connector.register(
MessageEndpoints.gui_instruction_response(request_id),
cb=self._on_rpc_response,
parent=self,
)
self._client.connector.set_and_publish(MessageEndpoints.gui_instructions(receiver), rpc_msg)
if wait_for_rpc_response:
response = redis_msg.wait(timeout)
if response is None:
raise RPCResponseTimeoutError(request_id, timeout)
try:
finished = self._msg_wait_event.wait(10)
if not finished:
raise RPCResponseTimeoutError(request_id, timeout)
finally:
self._msg_wait_event.clear()
self._client.connector.unregister(
MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response
)
# get class name
if not response.accepted:
raise ValueError(response.message["error"])
msg_result = response.message.get("result")
if not self._rpc_response.accepted:
raise ValueError(self._rpc_response.message["error"])
msg_result = self._rpc_response.message.get("result")
self._rpc_response = None
return self._create_widget_from_msg_result(msg_result)
@staticmethod
def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None:
msg = msg.value
parent._msg_wait_event.set()
parent._rpc_response = msg
def _create_widget_from_msg_result(self, msg_result):
if msg_result is None:
return None

View File

@@ -46,6 +46,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
"w7": self.w7,
"w8": self.w8,
"w9": self.w9,
"w10": self.w10,
"d0": self.d0,
"d1": self.d1,
"d2": self.d2,

View File

@@ -0,0 +1,47 @@
from bec_lib.serialization import MsgpackSerialization
from bec_lib.utils import lazy_import_from
from qtpy.QtCore import QEventLoop, QSocketNotifier, QTimer
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
class QtRedisMessageWaiter:
def __init__(self, redis_connector, message_to_wait):
self.ev_loop = QEventLoop()
self.response = None
self.connector = redis_connector
self.message_to_wait = message_to_wait
self.pubsub = redis_connector._redis_conn.pubsub()
self.pubsub.subscribe(self.message_to_wait.endpoint)
fd = self.pubsub.connection._sock.fileno()
self.notifier = QSocketNotifier(fd, QSocketNotifier.Read)
self.notifier.activated.connect(self._pubsub_readable)
def _msg_received(self, msg_obj):
self.response = msg_obj.value
self.ev_loop.quit()
def wait(self, timeout=1):
timer = QTimer()
timer.singleShot(timeout * 1000, self.ev_loop.quit)
self.ev_loop.exec_()
timer.stop()
self.notifier.setEnabled(False)
self.pubsub.close()
return self.response
def _pubsub_readable(self, fd):
while True:
msg = self.pubsub.get_message()
if msg:
if msg["type"] == "subscribe":
# get_message buffers, so we may already have the answer
# let's check...
continue
else:
break
else:
return
channel = msg["channel"].decode()
msg = MessageObject(topic=channel, value=MsgpackSerialization.loads(msg["data"]))
self.connector._execute_callback(self._msg_received, msg, {})

View File

@@ -106,3 +106,14 @@ class SettingsDialog(QDialog):
Apply the changes made in the settings widget without closing the dialog.
"""
self.widget.accept_changes()
def cleanup(self):
"""
Cleanup the dialog.
"""
self.button_box.close()
self.button_box.deleteLater()
def closeEvent(self, event):
self.cleanup()
super().closeEvent(event)

View File

@@ -8,7 +8,7 @@ import redis
from bec_lib.client import BECClient
from bec_lib.redis_connector import MessageObject, RedisConnector
from bec_lib.service_config import ServiceConfig
from qtpy.QtCore import PYQT6, PYSIDE6, QCoreApplication, QObject
from qtpy.QtCore import QObject
from qtpy.QtCore import Signal as pyqtSignal
if TYPE_CHECKING:
@@ -75,7 +75,6 @@ class BECDispatcher:
_instance = None
_initialized = False
qapp = None
def __new__(cls, client=None, config: str = None, *args, **kwargs):
if cls._instance is None:
@@ -87,9 +86,6 @@ class BECDispatcher:
if self._initialized:
return
if not QCoreApplication.instance():
BECDispatcher.qapp = QCoreApplication([])
self._slots = collections.defaultdict(set)
self.client = client
@@ -123,16 +119,6 @@ class BECDispatcher:
cls._instance = None
cls._initialized = False
if not cls.qapp:
return
# shutdown QCoreApp if it exists
if PYQT6:
cls.qapp.exit()
elif PYSIDE6:
cls.qapp.shutdown()
cls.qapp = None
def connect_slot(
self,
slot: Callable,
@@ -162,9 +148,9 @@ class BECDispatcher:
return
self.client.connector.unregister(topics, cb=connected_slot)
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
self._slots[slot].difference_update(set(topics_str))
if not self._slots[slot]:
del self._slots[slot]
self._slots[connected_slot].difference_update(set(topics_str))
if not self._slots[connected_slot]:
del self._slots[connected_slot]
def disconnect_topics(self, topics: Union[str, list]):
self.client.connector.unregister(topics)

View File

@@ -34,3 +34,10 @@ class ColorButton(pg.ColorButton):
return self.color().getRgb()
if format == "HEX":
return self.color().name()
def cleanup(self):
"""
Clean up the ColorButton.
"""
self.colorDialog.close()
self.colorDialog.deleteLater()

View File

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

View File

@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Literal, Optional
from pydantic import Field
from pyqtgraph.dockarea import Dock, DockLabel
from qtpy import QtCore, QtGui
from bec_widgets.cli.rpc_wigdet_handler import widget_handler
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
@@ -12,8 +13,6 @@ from bec_widgets.utils.bec_widget import BECWidget
if TYPE_CHECKING:
from qtpy.QtWidgets import QWidget
from bec_widgets.widgets.dock import BECDockArea
class DockConfig(ConnectionConfig):
widgets: dict[str, Any] = Field({}, description="The widgets in the dock.")
@@ -26,6 +25,23 @@ class DockConfig(ConnectionConfig):
class CustomDockLabel(DockLabel):
def __init__(self, text: str, closable: bool = True):
super().__init__(text, closable)
if closable:
red_icon = QtGui.QIcon()
pixmap = QtGui.QPixmap(32, 32)
pixmap.fill(QtCore.Qt.GlobalColor.red)
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen(QtCore.Qt.GlobalColor.white)
pen.setWidth(2)
painter.setPen(pen)
painter.drawLine(8, 8, 24, 24)
painter.drawLine(24, 8, 8, 24)
painter.end()
red_icon.addPixmap(pixmap)
self.closeButton.setIcon(red_icon)
def updateStyle(self):
r = "3px"
if self.dim:
@@ -137,6 +153,7 @@ class BECDock(BECWidget, Dock):
super().dropEvent(event)
if old_area in self.orig_area.tempAreas and old_area != self.orig_area:
self.orig_area.removeTempArea(old_area)
old_area.window().deleteLater()
def float(self):
"""
@@ -268,7 +285,7 @@ class BECDock(BECWidget, Dock):
"""
Attach the dock to the parent dock area.
"""
self.orig_area.removeTempArea(self.area)
self.parent_dock_area.remove_temp_area(self.area)
def detach(self):
"""
@@ -303,6 +320,8 @@ class BECDock(BECWidget, Dock):
if hasattr(widget, "cleanup"):
widget.cleanup()
self.widgets.clear()
self.label.close()
self.label.deleteLater()
super().cleanup()
def close(self):

View File

@@ -83,8 +83,8 @@ class BECDockArea(BECWidget, QWidget):
"scan_control": IconAction(
icon_path="scan_control.svg", tooltip="Add Scan Control"
),
"device_box": IconAction(
icon_path="device_box.svg", tooltip="Add Device Box"
"positioner_box": IconAction(
icon_path="positioner_box.svg", tooltip="Add Device Box"
),
},
),
@@ -132,8 +132,8 @@ class BECDockArea(BECWidget, QWidget):
self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect(
lambda: self.add_dock(widget="ScanControl", prefix="scan_control")
)
self.toolbar.widgets["menu_devices"].widgets["device_box"].triggered.connect(
lambda: self.add_dock(widget="DeviceBox", prefix="device_box")
self.toolbar.widgets["menu_devices"].widgets["positioner_box"].triggered.connect(
lambda: self.add_dock(widget="PositionerBox", prefix="positioner_box")
)
# Menu Utils
@@ -231,6 +231,7 @@ class BECDockArea(BECWidget, QWidget):
self.config.docks.pop(name, None)
if dock:
dock.close()
dock.deleteLater()
if len(self.dock_area.docks) <= 1:
for dock in self.dock_area.docks.values():
dock.hide_title_bar()
@@ -329,7 +330,16 @@ class BECDockArea(BECWidget, QWidget):
"""
while self.dock_area.tempAreas:
for temp_area in self.dock_area.tempAreas:
self.dock_area.removeTempArea(temp_area)
self.remove_temp_area(temp_area)
def remove_temp_area(self, area):
"""
Remove a temporary area from the dock area.
This is a patched method of pyqtgraph's removeTempArea
"""
self.dock_area.tempAreas.remove(area)
area.window().close()
area.window().deleteLater()
def clear_all(self):
"""
@@ -345,6 +355,10 @@ class BECDockArea(BECWidget, QWidget):
Cleanup the dock area.
"""
self.clear_all()
self.toolbar.close()
self.toolbar.deleteLater()
self.dock_area.close()
self.dock_area.deleteLater()
super().cleanup()
def close(self):

View File

@@ -513,6 +513,13 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
if widget_id in self._widgets:
raise ValueError(f"Widget with ID '{widget_id}' already exists.")
# Check if position is occupied
if row is not None and col is not None:
if self.getItem(row, col):
raise ValueError(f"Position at row {row} and column {col} is already occupied.")
else:
row, col = self._find_next_empty_position()
widget = self.widget_handler.create_widget(
widget_type=widget_type,
widget_id=widget_id,
@@ -525,23 +532,11 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
# used otherwise multiple times
widget.set_gui_id(widget_id)
# Check if position is occupied
if row is not None and col is not None:
if self.getItem(row, col):
raise ValueError(f"Position at row {row} and column {col} is already occupied.")
widget.config.row = row
widget.config.col = col
widget.config.row = row
widget.config.col = col
# Add widget to the figure
self.addItem(widget, row=row, col=col)
else:
row, col = self._find_next_empty_position()
widget.config.row = row
widget.config.col = col
# Add widget to the figure
self.addItem(widget, row=row, col=col)
# Add widget to the figure
self.addItem(widget, row=row, col=col)
# Update num_cols and num_rows based on the added widget
self.config.num_rows = max(self.config.num_rows, row + 1)
@@ -620,6 +615,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
"""
if widget_id in self._widgets:
widget = self._widgets.pop(widget_id)
widget.cleanup_pyqtgraph()
widget.cleanup()
self.removeItem(widget)
self.grid[widget.config.row][widget.config.col] = None
@@ -745,3 +741,12 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
self.config = FigureConfig(
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
)
def cleanup_pyqtgraph_all_widgets(self):
"""Clean up the pyqtgraph widget."""
for widget in self.widget_list:
widget.cleanup_pyqtgraph()
def cleanup(self):
"""Close the figure widget."""
self.cleanup_pyqtgraph_all_widgets()

View File

@@ -251,9 +251,10 @@ class BECImageShow(BECPlotBase):
image_exits = self._check_image_id(monitor, self._images)
if image_exits:
raise ValueError(
f"Monitor with ID '{monitor}' already exists in widget '{self.gui_id}'."
)
# raise ValueError(
# f"Monitor with ID '{monitor}' already exists in widget '{self.gui_id}'."
# )
return
# monitor = self.entry_validator.validate_monitor(monitor)
@@ -577,10 +578,10 @@ class BECImageShow(BECPlotBase):
self, source: str, name: str, config: ImageItemConfig, data=None
) -> BECImageItem: # TODO fix types
config.parent_id = self.gui_id
image = BECImageItem(config=config, parent_image=self)
self.plot_item.addItem(image)
if self.single_image is True and len(self.images) > 0:
self.remove_image(0)
image = BECImageItem(config=config, parent_image=self)
self.plot_item.addItem(image)
self._images[source][name] = image
if source == "device_monitor_2d":
self._connect_device_monitor_2d(config.monitor)
@@ -681,3 +682,17 @@ class BECImageShow(BECPlotBase):
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
)
self.images.clear()
def cleanup_pyqtgraph(self):
"""Cleanup pyqtgraph items."""
super().cleanup_pyqtgraph()
item = self.plot_item
if not item.items:
return
cbar = item.items[0].color_bar
cbar.vb.menu.close()
cbar.vb.menu.deleteLater()
cbar.gradient.menu.close()
cbar.gradient.menu.deleteLater()
cbar.gradient.colorDialog.close()
cbar.gradient.colorDialog.deleteLater()

View File

@@ -314,3 +314,11 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
"""Remove the plot widget from the figure."""
if self.figure is not None:
self.figure.remove(widget_id=self.gui_id)
def cleanup_pyqtgraph(self):
"""Cleanup pyqtgraph items."""
item = self.plot_item
item.vb.menu.close()
item.vb.menu.deleteLater()
item.ctrlMenu.close()
item.ctrlMenu.deleteLater()

View File

@@ -943,7 +943,9 @@ class BECWaveform(BECPlotBase):
self.setup_dap(self.old_scan_id, self.scan_id)
if self._curves_data["async"]:
for curve_id, curve in self._curves_data["async"].items():
self.setup_async(curve.config.signals.y.name)
self.setup_async(
name=curve.config.signals.y.name, entry=curve.config.signals.y.entry
)
@Slot(dict, dict)
def on_scan_segment(self, msg: dict, metadata: dict):
@@ -1005,18 +1007,18 @@ class BECWaveform(BECPlotBase):
)
@Slot(str)
def setup_async(self, device: str):
def setup_async(self, name: str, entry: str):
self.bec_dispatcher.disconnect_slot(
self.on_async_readback, MessageEndpoints.device_async_readback(self.old_scan_id, device)
self.on_async_readback, MessageEndpoints.device_async_readback(self.old_scan_id, name)
)
try:
self._curves_data["async"][f"{device}-{device}"].clear_data()
self._curves_data["async"][f"{name}-{entry}"].clear_data()
except KeyError:
pass
if len(self._curves_data["async"]) > 0:
self.bec_dispatcher.connect_slot(
self.on_async_readback,
MessageEndpoints.device_async_readback(self.scan_id, device),
MessageEndpoints.device_async_readback(self.scan_id, name),
from_start=True,
)

View File

@@ -457,7 +457,8 @@ class BECImageWidget(BECWidget, QWidget):
def cleanup(self):
self.fig.cleanup()
self.client.shutdown()
self.toolbar.close()
self.toolbar.deleteLater()
return super().cleanup()

View File

@@ -63,8 +63,6 @@ class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
def closeEvent(self, event):
self.shutdown_kernel()
if self.client:
self.client.shutdown()
if __name__ == "__main__": # pragma: no cover

View File

@@ -45,3 +45,12 @@ class MotorMapSettings(SettingWidget):
self.target_widget.set_scatter_size(scatter_size)
self.target_widget.set_background_value(background_intensity)
self.target_widget.set_color(color)
def cleanup(self):
self.ui.color.cleanup()
self.ui.color.close()
self.ui.color.deleteLater()
def closeEvent(self, event):
self.cleanup()
super().closeEvent(event)

View File

@@ -1,22 +1,43 @@
""" Module for a PositionerBox widget to control a positioner device."""
import os
import uuid
from bec_lib.device import Positioner
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
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 qtpy.QtCore import Property, QSize, Signal, Slot
from qtpy.QtGui import QDoubleValidator, QIcon
from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QPushButton, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
logger = bec_logger.logger
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class DeviceBox(BECWidget, QWidget):
class PositionerBox(BECWidget, QWidget):
"""Simple Widget to control a positioner in box form"""
ui_file = "positioner_box.ui"
dimensions = (234, 224)
USER_ACCESS = ["set_positioner"]
device_changed = Signal(str, str)
def __init__(self, parent=None, device=None, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, parent=None, device: Positioner = None, *args, **kwargs):
"""Initialize the PositionerBox widget.
Args:
parent: The parent widget.
device (Positioner): The device to control.
"""
super().__init__(**kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts()
self._device = ""
@@ -29,10 +50,11 @@ class DeviceBox(BECWidget, QWidget):
self.init_device()
def init_ui(self):
"""Init the ui"""
self.device_changed.connect(self.on_device_change)
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, "device_box.ui"))
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
@@ -41,8 +63,8 @@ class DeviceBox(BECWidget, QWidget):
# fix the size of the device box
db = self.ui.device_box
db.setFixedHeight(234)
db.setFixedWidth(224)
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)
@@ -55,30 +77,105 @@ class DeviceBox(BECWidget, QWidget):
self.setpoint_validator = QDoubleValidator()
self.ui.setpoint.setValidator(self.setpoint_validator)
self.ui.spinner_widget.start()
self.ui.tool_button.clicked.connect(self._open_dialog_selection)
icon = QIcon()
icon.addFile(
os.path.join(MODULE_PATH, "assets", "toolbar_icons", "device_line_edit.svg"),
size=QSize(16, 16),
)
self.ui.tool_button.setIcon(icon)
def _open_dialog_selection(self):
"""Open dialog window for positioner selection"""
dialog = QDialog(self)
dialog.setWindowTitle("Positioner Selection")
layout = QVBoxLayout()
line_edit = DeviceLineEdit(self, client=self.client, device_filter="Positioner")
line_edit.textChanged.connect(self._positioner_changed)
layout.addWidget(line_edit)
close_button = QPushButton("Close")
close_button.clicked.connect(dialog.accept)
layout.addWidget(close_button)
dialog.setLayout(layout)
dialog.exec()
@Slot(str)
def _positioner_changed(self, positioner_name: str):
"""Changed input in combobox.
Args:
positioner_name (str): name of the positioner
"""
self.set_positioner(positioner_name)
def init_device(self):
if self.device in self.dev:
"""Init the device view and readback"""
if self._check_device_is_valid(self.device):
data = self.dev[self.device].read()
self.on_device_readback({"signals": data}, {})
def _toogle_enable_buttons(self, enable: bool) -> None:
"""Toogle enable/disable on available buttons
Args:
enable (bool): Enable buttons
"""
self.ui.tweak_left.setEnabled(enable)
self.ui.tweak_right.setEnabled(enable)
self.ui.stop.setEnabled(enable)
self.ui.setpoint.setEnabled(enable)
self.ui.step_size.setEnabled(enable)
@Property(str)
def device(self):
"""Property to set the device"""
return self._device
@device.setter
def device(self, value):
def device(self, value: str):
"""Setter, checks if device is a string"""
if not value or not isinstance(value, str):
return
old_device = self._device
self._device = value
self.device_changed.emit(old_device, value)
def set_positioner(self, positioner: str):
"""Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
if isinstance(positioner, Positioner):
positioner = positioner.name
self.device = positioner
def _check_device_is_valid(self, device: str):
"""Check if the device is a positioner
Args:
device (str): The device name
"""
if device not in self.dev:
logger.info(f"Device {device} not found in the device list")
return False
if not isinstance(self.dev[device], Positioner):
logger.info(f"Device {device} is not a positioner")
return False
return True
@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")
"""Upon changing the device, a check will be performed if the device is a Positioner.
Args:
old_device (str): The old device name.
new_device (str): The new device name.
"""
if not self._check_device_is_valid(new_device):
return
print(f"Device changed from {old_device} to {new_device}")
logger.info(f"Device changed from {old_device} to {new_device}")
self._toogle_enable_buttons(True)
self.init_device()
self.bec_dispatcher.disconnect_slot(
self.on_device_readback, MessageEndpoints.device_readback(old_device)
@@ -98,6 +195,12 @@ class DeviceBox(BECWidget, QWidget):
@Slot(dict, dict)
def on_device_readback(self, msg_content: dict, metadata: dict):
"""Callback for device readback.
Args:
msg_content (dict): The message content.
metadata (dict): The message metadata.
"""
signals = msg_content.get("signals", {})
# pylint: disable=protected-access
hinted_signals = self.dev[self.device]._hints
@@ -134,7 +237,12 @@ class DeviceBox(BECWidget, QWidget):
pos = (readback_val - limits[0]) / (limits[1] - limits[0])
self.ui.position_indicator.on_position_update(pos)
def update_limits(self, limits):
def update_limits(self, limits: tuple):
"""Update limits
Args:
limits (tuple): Limits of the positioner
"""
if limits == self._limits:
return
self._limits = limits
@@ -147,6 +255,7 @@ class DeviceBox(BECWidget, QWidget):
@Slot()
def on_stop(self):
"""Stop call"""
request_id = str(uuid.uuid4())
params = {
"device": self.device,
@@ -165,18 +274,22 @@ class DeviceBox(BECWidget, QWidget):
@property
def step_size(self):
"""Step size for tweak"""
return self.ui.step_size.value()
@Slot()
def on_tweak_right(self):
"""Tweak motor right"""
self.dev[self.device].move(self.step_size, relative=True)
@Slot()
def on_tweak_left(self):
"""Tweak motor left"""
self.dev[self.device].move(-self.step_size, relative=True)
@Slot()
def on_setpoint_change(self):
"""Change the setpoint for the motor"""
self.ui.setpoint.clearFocus()
setpoint = self.ui.setpoint.text()
self.dev[self.device].move(float(setpoint), relative=False)
@@ -190,8 +303,8 @@ if __name__ == "__main__": # pragma: no cover
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
apply_theme("light")
widget = DeviceBox(device="samx")
apply_theme("dark")
widget = PositionerBox(device="bpm4i")
widget.show()
sys.exit(app.exec_())

View File

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

View File

@@ -29,17 +29,24 @@
<item>
<widget class="QGroupBox" name="device_box">
<property name="title">
<string>Device Name</string>
<string>No positioner selected</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"/>
<widget class="QDoubleSpinBox" name="step_size">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="tweak_right">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
@@ -67,10 +74,20 @@
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QLineEdit" name="setpoint"/>
<widget class="QLineEdit" name="setpoint">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QToolButton" name="tweak_left">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
@@ -99,6 +116,9 @@
</item>
<item row="4" column="0" colspan="3">
<widget class="QPushButton" name="stop">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Stop</string>
</property>
@@ -108,6 +128,13 @@
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QToolButton" name="tool_button">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">

View File

@@ -1,30 +1,29 @@
# 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
import bec_widgets
from bec_widgets.widgets.device_box.device_box import DeviceBox
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
DOM_XML = """
<ui language='c++'>
<widget class='DeviceBox' name='device_box'>
<widget class='PositionerBox' name='positioner_box'>
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = DeviceBox(parent)
t = PositionerBox(parent)
return t
def domXml(self):
@@ -34,11 +33,11 @@ class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "Device Control"
def icon(self):
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "device_box.png")
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "positioner_box.png")
return QIcon(icon_path)
def includeFile(self):
return "device_box"
return "positioner_box"
def initialize(self, form_editor):
self._form_editor = form_editor
@@ -50,10 +49,10 @@ class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return self._form_editor is not None
def name(self):
return "DeviceBox"
return "PositionerBox"
def toolTip(self):
return "A widget for controlling a single positioner. "
return "Simple Widget to control a positioner in box form"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,33 @@
from bec_lib.device import Positioner
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
class PositionerControlLine(PositionerBox):
"""A widget that controls a single device."""
ui_file = "positioner_control_line.ui"
dimensions = (60, 600) # height, width
def __init__(self, parent=None, device: Positioner = None, *args, **kwargs):
"""Initialize the DeviceControlLine.
Args:
parent: The parent widget.
device (Positioner): The device to control.
"""
super().__init__(parent=parent, device=device, *args, **kwargs)
if __name__ == "__main__": # pragma: no cover
import sys
import qdarktheme
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
qdarktheme.setup_theme("dark")
widget = PositionerControlLine(device="samy")
widget.show()
sys.exit(app.exec_())

View File

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

View File

@@ -0,0 +1,216 @@
<?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>
<widget class="QToolButton" name="tool_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="PositionIndicator" name="position_indicator">
<property name="minimumSize">
<size>
<width>80</width>
<height>10</height>
</size>
</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>
<item>
<widget class="QLabel" name="readback">
<property name="minimumSize">
<size>
<width>80</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>80</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</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>
</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,58 @@
# 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.positioner_box.positioner_control_line import PositionerControlLine
DOM_XML = """
<ui language='c++'>
<widget class='PositionerControlLine' name='positioner_control_line'>
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = PositionerControlLine(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "Device Control"
def icon(self):
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "positioner_box.png")
return QIcon(icon_path)
def includeFile(self):
return "positioner_control_line"
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 "PositionerControlLine"
def toolTip(self):
return "A widget that controls a single positioner in line form."
def whatsThis(self):
return self.toolTip()

View File

@@ -6,9 +6,9 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.device_box.device_box_plugin import DeviceBoxPlugin
from bec_widgets.widgets.positioner_box.positioner_box_plugin import PositionerBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceBoxPlugin())
QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerBoxPlugin())
if __name__ == "__main__": # pragma: no cover

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.positioner_box.positioner_control_line_plugin import (
PositionerControlLinePlugin,
)
QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerControlLinePlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -1,9 +1,13 @@
from typing import Literal
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QGridLayout,
QGroupBox,
QLabel,
@@ -25,13 +29,46 @@ class ScanArgType:
LITERALS = "dict"
class SettingsDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Settings")
layout = QFormLayout()
self.precision_spin_box = QSpinBox()
self.precision_spin_box.setRange(
-2147483647, 2147483647
) # 2147483647 is the largest int which qt allows
self.step_size_spin_box = QDoubleSpinBox()
self.step_size_spin_box.setRange(-float("inf"), float("inf"))
fixed_width = 80
self.precision_spin_box.setFixedWidth(fixed_width)
self.step_size_spin_box.setFixedWidth(fixed_width)
layout.addRow("Decimal Precision:", self.precision_spin_box)
layout.addRow("Step Size:", self.step_size_spin_box)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
def getValues(self):
return self.precision_spin_box.value(), self.step_size_spin_box.value()
class ScanSpinBox(QSpinBox):
def __init__(
self, parent=None, arg_name: str = None, default: int | None = None, *args, **kwargs
):
super().__init__(parent=parent, *args, **kwargs)
self.arg_name = arg_name
self.setRange(-9999, 9999)
self.setRange(-2147483647, 2147483647) # 2147483647 is the largest int which qt allows
if default is not None:
self.setValue(default)
@@ -42,10 +79,25 @@ class ScanDoubleSpinBox(QDoubleSpinBox):
):
super().__init__(parent=parent, *args, **kwargs)
self.arg_name = arg_name
self.setRange(-9999, 9999)
self.setRange(-float("inf"), float("inf"))
if default is not None:
self.setValue(default)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showSettingsDialog)
self.setToolTip("Right click to open settings dialog for decimal precision and step size.")
def showSettingsDialog(self):
dialog = SettingsDialog(self)
dialog.precision_spin_box.setValue(self.decimals())
dialog.step_size_spin_box.setValue(self.singleStep())
if dialog.exec_() == QDialog.Accepted:
precision, step_size = dialog.getValues()
self.setDecimals(precision)
self.setSingleStep(step_size)
class ScanLineEdit(QLineEdit):
def __init__(

View File

@@ -58,16 +58,17 @@ class CurveSettings(SettingWidget):
self.ui.color_map_selector_scan.combo.setCurrentText(cm)
# Scan Curve Table
for label, curve in config["scan_segment"].items():
row_count = self.ui.scan_table.rowCount()
self.ui.scan_table.insertRow(row_count)
DialogRow(
parent=self,
table_widget=self.ui.scan_table,
client=self.target_widget.client,
row=row_count,
config=curve.config,
).add_scan_row()
for source in ["scan_segment", "async"]:
for label, curve in config[source].items():
row_count = self.ui.scan_table.rowCount()
self.ui.scan_table.insertRow(row_count)
DialogRow(
parent=self,
table_widget=self.ui.scan_table,
client=self.target_widget.client,
row=row_count,
config=curve.config,
).add_scan_row()
# Add DAP Curves
for label, curve in config["DAP"].items():
@@ -132,11 +133,12 @@ class CurveSettings(SettingWidget):
self.accept_curve_changes()
def accept_curve_changes(self):
old_curves_scans = list(self.target_widget.waveform._curves_data["scan_segment"].values())
old_curves_dap = list(self.target_widget.waveform._curves_data["DAP"].values())
for curve in old_curves_scans:
curve.remove()
for curve in old_curves_dap:
sources = ["scan_segment", "async", "DAP"]
old_curves = []
for source in sources:
old_curves += list(self.target_widget.waveform._curves_data[source].values())
for curve in old_curves:
curve.remove()
self.get_curve_params()

View File

@@ -561,7 +561,6 @@ class BECWaveformWidget(BECWidget, QWidget):
def cleanup(self):
self.fig.cleanup()
self.client.shutdown()
return super().cleanup()

View File

@@ -64,6 +64,12 @@ class WebsiteWidget(BECWidget, QWebEngineView):
"""
QWebEngineView.forward(self)
def cleanup(self):
"""
Cleanup the widget
"""
self.page().deleteLater()
if __name__ == "__main__":
import sys

View File

@@ -1,7 +1,9 @@
(developer)=
# Developer
Welcome to the BEC Widgets developer guide! This section is intended for developers who want to contribute to the development of BEC Widgets.
Welcome to the BEC Widgets developer guide! BEC Widgets is a framework for building graphical user interfaces (GUIs) for [BEC](https://bec.readthedocs.io/en/latest/), a Python package for beamline experiment control.
This guide targets readers who want to develop new widgets or extend existing ones. If your goal is to use BEC Widgets to build GUIs for your experiments, please refer to the [user guide](#user).
```{toctree}
---
@@ -9,8 +11,8 @@ maxdepth: 2
hidden: true
---
getting_started/getting_started.md
widgets/widgets.md
introduction/introduction.md
widget_development/widget_development.md
api_reference/api_reference.md
```
@@ -21,26 +23,28 @@ api_reference/api_reference.md
:gutter: 5
```{grid-item-card}
:link: developer.getting_started
:link: developer.introduction
:link-type: ref
:img-top: /assets/rocket_launch_48dp.svg
:text-align: center
## Getting Started
## Introduction
Learn how to install BEC Widgets and get started with the framework.
An introduction into the single-resposibility principle and the modular design of BEC Widgets.
```
```{grid-item-card}
:link: developer.widgets
:link: developer.widget_development
:link-type: ref
:img-top: /assets/apps_48dp.svg
:text-align: center
## Widgets
## Widget Development
Learn about the building blocks of larger applications: widgets.
Learn how to develop a new modular widget for BEC Widgets.
```
````
````{grid} 2

View File

@@ -1,12 +0,0 @@
(developer.getting_started)=
# Getting Started
This section provides valuable information for developers who want to contribute to the development of BEC Widgets. The guide will help you set up the development environment, understand the modular development concept of BEC Widgets, and contribute to the project.
```{toctree}
---
maxdepth: 2
hidden: false
---
development/
```

View File

@@ -0,0 +1,14 @@
(developer.concepts)=
# Concepts
This section provides an overview of the core concepts of BEC Widgets, which are based on the single-responsibility principle and modular design.
## Moduler Design
We develop widgets with the single-responsibility principle in mind, meaning each widget is designed for a specific task. Our goal is to keep widgets simple, using them primarily for visualization or to initiate actions within BEC. Following these ideas, widgets should be designed to be reusable in various applications, making them versatile building blocks for larger GUIs.
We offer up to three different options for composing larger GUIs from these modular widgets: BECDesigner, DockArea widget, or scripting from the command line interface. More information about these options can be found in the user sections on [applications](user.applications).
## Client-Server Architecture
BEC Widgets is built on top of the [BEC](https://bec.readthedocs.io/en/latest/) package, which provides the backend for beamline experiment control. BEC Widgets is a client of BEC, meaning it can interact with the backend through a client-server architecture. To make full usage of the available features of BEC, we recommend to check the documentation about [data access](https://bec.readthedocs.io/en/latest/developer/data_access/data_access.html) in which the messaging and event system of BEC is described.
In the context of BEC Widgets, the [`BECDispatcher`](/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher) connects to this messaging and event system, allowing you to link your Qt [`Slots`](https://www.pythonguis.com/tutorials/pyside6-signals-slots-events/) to messages and event received from BEC.

View File

@@ -1,5 +1,5 @@
(developer.development)=
# Development
(developer.contributing)=
# Contributing
If you like to contribute to the development of BEC Widgets, you can follow the steps below to set up your development environment.
BEC Widgets works in conjunction with [BEC](https://bec.readthedocs.io/en/latest/).
@@ -10,6 +10,7 @@ If you already have a BEC environment set up, you can install BEC Widgets in edi
**Prerequisites**
1. **Python Version:** BEC Widgets requires Python version 3.10 or higher. Verify your Python version to ensure compatibility.
2. **BEC Installation:** BEC Widgets works in conjunction with BEC. While BEC is a dependency and will be installed automatically, you can find more information about BEC and its installation process in the [BEC documentation](https://beamline-experiment-control.readthedocs.io/en/latest/).
3. **Qt Distributions:** BEC Widgets supports [PySide6](https://doc.qt.io/qtforpython-6/quickstart.html) and [PyQt6](https://www.riverbankcomputing.com/static/Docs/PyQt6/introduction.html). We use [qtpy](https://pypi.org/project/QtPy/) to abstract the underlying QT distribution.
**Clone the Repository**:
```bash
@@ -20,8 +21,8 @@ cd bec_widgets
Please install the package in editable mode into your BEC Python environemnt.
```bash
pip install -e '.[dev,pyqt6]'
pip install -e '.[dev,pyside6]'
```
This installs the package together with [PyQT6](https://www.riverbankcomputing.com/static/Docs/PyQt6/introduction.html).
This installs the package together with [PySide6](https://doc.qt.io/qtforpython-6/quickstart.html).

View File

@@ -0,0 +1,15 @@
(developer.introduction)=
# Introduction
BEC Widgets is a framework providing modular components that can be integrated into various larger GUI applications. Each widget serves a specific purpose adhering to the single-responsibility principle as detailed in our [concepts section](developer.concepts), and offers a straightforward, user-friendly interface.
These widgets provide data visualisation tools and experimental control interfaces by triggering actions within BEC.
If you're interested in contributing to BEC Widgets, please refer to our [contributing guide](developer.contributing), which provides instructions for setting up your development environment and creating your own widgets.
```{toctree}
---
maxdepth: 2
hidden: false
---
concepts/
contributing/
```

View File

@@ -1,5 +1,5 @@
(developer.widgets)=
# Widgets
(developer.widget_development)=
# Widget development
This section provides an introduction to the building blocks of BEC Widgets: widgets. Widgets are the basic components of the graphical user interface (GUI) and are used to create larger applications. We will cover key topics such as how to develop new widgets or how to customise existing widgets. For details on the already available widgets and their usage, please refer to user section about [widgets](#user.widgets)
```{toctree}

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "0.93.0"
version = "0.94.5"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [
@@ -13,16 +13,16 @@ classifiers = [
"Topic :: Scientific/Engineering",
]
dependencies = [
"bec_ipython_client~=2.16", # needed for jupyter console
"bec_lib~=2.16",
"black~=24.0", # needed for bw-generate-cli
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
"bec_ipython_client~=2.21, >=2.21.4", # needed for jupyter console
"bec_lib~=2.21, >=2.21.4",
"black~=24.0", # needed for bw-generate-cli
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
"pydantic~=2.0",
"pyqtgraph~=0.13",
"bec_qthemes~=0.0",
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
"qtpy~=2.4",
"pyte", # needed for vt100 console
"pyte", # needed for vt100 console
]

View File

@@ -9,6 +9,10 @@ from bec_widgets.cli.auto_updates import AutoUpdates
from bec_widgets.cli.client import BECDockArea, BECFigure, BECImageShow, BECMotorMap, BECWaveform
from bec_widgets.utils import Colors
# pylint: disable=unused-argument
# pylint: disable=redefined-outer-name
# pylint: disable=too-many-locals
def test_rpc_add_dock_with_figure_e2e(bec_client_lib, rpc_server_dock):
# BEC client shortcuts
@@ -227,13 +231,20 @@ def test_ring_bar_scan_update(bec_client_lib, rpc_server_dock):
assert bar_config["rings"][1]["max_value"] == final_samy
def test_auto_update(bec_client_lib, rpc_server_dock):
def test_auto_update(bec_client_lib, rpc_server_dock, qtbot):
dock = BECDockArea(rpc_server_dock)
AutoUpdates.enabled = True
AutoUpdates.create_default_dock = True
dock.auto_updates = AutoUpdates(gui=dock)
dock.auto_updates.start_default_dock()
def get_default_figure():
return dock.auto_updates.get_default_figure()
qtbot.waitUntil(lambda: get_default_figure() is not None, timeout=10000)
plt = get_default_figure()
dock.selected_device = "bpm4i"
# we need to start the update script manually; normally this is done when the GUI is started
@@ -250,7 +261,6 @@ def test_auto_update(bec_client_lib, rpc_server_dock):
last_scan_data = queue.scan_storage.storage[-1].data
# get data from curves
plt = dock.auto_updates.get_default_figure()
widgets = plt.widget_list
plt_data = widgets[0].get_all_data()
@@ -271,6 +281,7 @@ def test_auto_update(bec_client_lib, rpc_server_dock):
plt = dock.auto_updates.get_default_figure()
widgets = plt.widget_list
qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
plt_data = widgets[0].get_all_data()
last_scan_data = queue.scan_storage.storage[-1].data
@@ -284,3 +295,4 @@ def test_auto_update(bec_client_lib, rpc_server_dock):
plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["y"]
== last_scan_data["samy"]["samy"].val
)
dock.auto_updates.shutdown()

View File

@@ -158,7 +158,7 @@ def test_rpc_motor_map(rpc_server_figure, bec_client_lib):
)
def test_dap_rpc(rpc_server_figure, bec_client_lib):
def test_dap_rpc(rpc_server_figure, bec_client_lib, qtbot):
fig = BECFigure(rpc_server_figure)
plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
@@ -178,13 +178,15 @@ def test_dap_rpc(rpc_server_figure, bec_client_lib):
res = scans.line_scan(dev.samx, 0, 8, steps=50, relative=False)
res.wait()
time.sleep(2)
# especially on slow machines, the fit might not be done yet
# so we wait until the fit reaches the expected value
def wait_for_fit():
dap_curve = plt.get_curve("bpm4i-bpm4i-GaussianModel")
fit_params = dap_curve.dap_params
print(fit_params)
return np.isclose(fit_params["center"], 5, atol=0.5)
dap_curve = plt.get_curve("bpm4i-bpm4i-GaussianModel")
fit_params = dap_curve.dap_params
print(fit_params)
assert np.isclose(fit_params["center"], 5, atol=0.5)
qtbot.waitUntil(wait_for_fit, timeout=10000)
def test_removing_subplots(rpc_server_figure, bec_client_lib):

View File

@@ -12,7 +12,6 @@ def scan_control(qtbot, bec_client_lib): # , mock_dev):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_scan_control_populate_scans_e2e(scan_control):

View File

@@ -1,4 +1,6 @@
import pytest
from pytestqt.exceptions import TimeoutError as QtBotTimeoutError
from qtpy.QtWidgets import QApplication
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.qt_utils import error_popups
@@ -6,9 +8,16 @@ from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
@pytest.fixture(autouse=True)
def qapplication(qapp): # pylint: disable=unused-argument
def qapplication(qtbot): # pylint: disable=unused-argument
yield
qapp.processEvents() # make sure all events are processed before shutting down
qapp = QApplication.instance()
# qapp.quit()
qapp.processEvents()
try:
qtbot.waitUntil(lambda: qapp.topLevelWidgets() == [])
except QtBotTimeoutError as exc:
raise TimeoutError(f"Failed to close all widgets: {qapp.topLevelWidgets()}") from exc
@pytest.fixture(autouse=True)
@@ -31,3 +40,25 @@ def bec_dispatcher(threads_check): # pylint: disable=unused-argument
@pytest.fixture(autouse=True)
def clean_singleton():
error_popups._popup_utility_instance = None
def create_widget(qtbot, widget, *args, **kwargs):
"""
Create a widget and add it to the qtbot for testing. This is a helper function that
should be used in all tests that require a widget to be created.
DO NOT CREATE WIDGETS DIRECTLY IN A FIXTURE!
Args:
qtbot (fixture): pytest-qt fixture
widget (QWidget): widget class to be created
*args: positional arguments for the widget
**kwargs: keyword arguments for the widget
Returns:
QWidget: the created widget
"""
widget = widget(*args, **kwargs)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
return widget

View File

@@ -92,7 +92,9 @@ def test_dispatcher_2_cb_same_topic(bec_dispatcher_w_connector, qtbot, send_msg_
bec_dispatcher.connect_slot(cb1, "topic1")
bec_dispatcher.connect_slot(cb2, "topic1")
assert len(bec_dispatcher.client.connector._topics_cb) == 1
assert len(bec_dispatcher._slots) == 2
bec_dispatcher.disconnect_slot(cb1, "topic1")
assert len(bec_dispatcher._slots) == 1
send_msg_event.set()
qtbot.wait(10)

View File

@@ -14,7 +14,6 @@ def bec_dock_area(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_bec_dock_area_init(bec_dock_area):
@@ -128,10 +127,12 @@ def test_toolbar_add_plot_motor_map(bec_dock_area):
assert bec_dock_area.panels["motor_map_1"].widgets[0].config.widget_class == "BECMotorMapWidget"
def test_toolbar_add_device_device_box(bec_dock_area):
bec_dock_area.toolbar.widgets["menu_devices"].widgets["device_box"].trigger()
assert "device_box_1" in bec_dock_area.panels
assert bec_dock_area.panels["device_box_1"].widgets[0].config.widget_class == "DeviceBox"
def test_toolbar_add_device_positioner_box(bec_dock_area):
bec_dock_area.toolbar.widgets["menu_devices"].widgets["positioner_box"].trigger()
assert "positioner_box_1" in bec_dock_area.panels
assert (
bec_dock_area.panels["positioner_box_1"].widgets[0].config.widget_class == "PositionerBox"
)
def test_toolbar_add_utils_queue(bec_dock_area):

View File

@@ -9,18 +9,11 @@ from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap
from bec_widgets.widgets.figure.plots.waveform.waveform import BECWaveform
from .client_mocks import mocked_client
from .conftest import create_widget
@pytest.fixture
def bec_figure(qtbot, mocked_client):
widget = BECFigure(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_bec_figure_init(bec_figure):
def test_bec_figure_init(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
assert bec_figure is not None
assert bec_figure.client is not None
assert isinstance(bec_figure, BECFigure)
@@ -34,7 +27,8 @@ def test_bec_figure_init_with_config(mocked_client):
assert widget.config.theme == "dark"
def test_bec_figure_add_remove_plot(bec_figure):
def test_bec_figure_add_remove_plot(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
initial_count = len(bec_figure._widgets)
# Adding 3 widgets - 2 WaveformBase and 1 PlotBase
@@ -64,7 +58,8 @@ def test_bec_figure_add_remove_plot(bec_figure):
assert bec_figure._widgets[w1.gui_id].config.widget_class == "BECWaveform"
def test_add_different_types_of_widgets(bec_figure):
def test_add_different_types_of_widgets(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
plt = bec_figure.plot(x_name="samx", y_name="bpm4i")
im = bec_figure.image("eiger")
motor_map = bec_figure.motor_map("samx", "samy")
@@ -74,7 +69,8 @@ def test_add_different_types_of_widgets(bec_figure):
assert motor_map.__class__ == BECMotorMap
def test_access_widgets_access_errors(bec_figure):
def test_access_widgets_access_errors(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
bec_figure.plot(row=0, col=0)
# access widget by non-existent coordinates
@@ -96,7 +92,8 @@ def test_access_widgets_access_errors(bec_figure):
)
def test_add_plot_to_occupied_position(bec_figure):
def test_add_plot_to_occupied_position(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
bec_figure.plot(row=0, col=0)
with pytest.raises(ValueError) as excinfo:
@@ -104,7 +101,8 @@ def test_add_plot_to_occupied_position(bec_figure):
assert "Position at row 0 and column 0 is already occupied." in str(excinfo.value)
def test_remove_plots(bec_figure):
def test_remove_plots(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot(row=0, col=0)
w2 = bec_figure.plot(row=0, col=1)
w3 = bec_figure.plot(row=1, col=0)
@@ -134,7 +132,8 @@ def test_remove_plots(bec_figure):
assert len(bec_figure._widgets) == 1
def test_remove_plots_by_coordinates_ints(bec_figure):
def test_remove_plots_by_coordinates_ints(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot(row=0, col=0)
w2 = bec_figure.plot(row=0, col=1)
@@ -145,7 +144,8 @@ def test_remove_plots_by_coordinates_ints(bec_figure):
assert len(bec_figure._widgets) == 1
def test_remove_plots_by_coordinates_tuple(bec_figure):
def test_remove_plots_by_coordinates_tuple(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot(row=0, col=0)
w2 = bec_figure.plot(row=0, col=1)
@@ -156,7 +156,8 @@ def test_remove_plots_by_coordinates_tuple(bec_figure):
assert len(bec_figure._widgets) == 1
def test_remove_plot_by_id_error(bec_figure):
def test_remove_plot_by_id_error(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
bec_figure.plot()
with pytest.raises(ValueError) as excinfo:
@@ -164,7 +165,8 @@ def test_remove_plot_by_id_error(bec_figure):
assert "Widget with ID 'non_existent_widget' does not exist." in str(excinfo.value)
def test_remove_plot_by_coordinates_error(bec_figure):
def test_remove_plot_by_coordinates_error(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
bec_figure.plot(row=0, col=0)
with pytest.raises(ValueError) as excinfo:
@@ -172,7 +174,8 @@ def test_remove_plot_by_coordinates_error(bec_figure):
assert "No widget at coordinates (0, 1)" in str(excinfo.value)
def test_remove_plot_by_providing_nothing(bec_figure):
def test_remove_plot_by_providing_nothing(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
bec_figure.plot(row=0, col=0)
with pytest.raises(ValueError) as excinfo:
@@ -192,7 +195,8 @@ def test_remove_plot_by_providing_nothing(bec_figure):
# assert bec_figure.backgroundBrush().color().name() == "#000000"
def test_change_layout(bec_figure):
def test_change_layout(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot(row=0, col=0)
w2 = bec_figure.plot(row=0, col=1)
w3 = bec_figure.plot(row=1, col=0)
@@ -215,7 +219,8 @@ def test_change_layout(bec_figure):
assert bec_figure[0, 3] == w4
def test_clear_all(bec_figure):
def test_clear_all(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
bec_figure.plot(row=0, col=0)
bec_figure.plot(row=0, col=1)
bec_figure.plot(row=1, col=0)
@@ -227,7 +232,8 @@ def test_clear_all(bec_figure):
assert np.shape(bec_figure.grid) == (0,)
def test_shortcuts(bec_figure):
def test_shortcuts(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
plt = bec_figure.plot(x_name="samx", y_name="bpm4i")
im = bec_figure.image("eiger")
motor_map = bec_figure.motor_map("samx", "samy")
@@ -240,7 +246,8 @@ def test_shortcuts(bec_figure):
assert motor_map.__class__ == BECMotorMap
def test_plot_access_factory(bec_figure):
def test_plot_access_factory(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
plt_00 = bec_figure.plot(x_name="samx", y_name="bpm4i")
plt_01 = bec_figure.plot(x_name="samx", y_name="bpm4i", row=0, col=1)
plt_10 = bec_figure.plot(new=True)

View File

@@ -6,8 +6,10 @@ import pytest
from bec_lib import messages
from qtpy.QtGui import QFontInfo
from bec_widgets.widgets.figure import BECFigure
from .client_mocks import mocked_client
from .test_bec_figure import bec_figure
from .conftest import create_widget
@pytest.fixture
@@ -15,7 +17,8 @@ def bec_image_show(bec_figure):
yield bec_figure.image("eiger")
def test_on_image_update(bec_image_show):
def test_on_image_update(qtbot, mocked_client):
bec_image_show = create_widget(qtbot, BECFigure, client=mocked_client).image("eiger")
data = np.random.rand(100, 100)
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
bec_image_show.on_image_update(msg.content, msg.metadata)
@@ -23,7 +26,8 @@ def test_on_image_update(bec_image_show):
assert np.array_equal(img.get_data(), data)
def test_autorange_on_image_update(bec_image_show):
def test_autorange_on_image_update(qtbot, mocked_client):
bec_image_show = create_widget(qtbot, BECFigure, client=mocked_client).image("eiger")
# Check if autorange mode "mean" works, should be default
data = np.random.rand(100, 100)
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})

View File

@@ -15,7 +15,6 @@ def image_widget(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.fixture

View File

@@ -2,14 +2,16 @@ import numpy as np
import pytest
from bec_lib.messages import DeviceMessage
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap, MotorMapConfig
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import SignalData
from .client_mocks import mocked_client
from .test_bec_figure import bec_figure
from .conftest import create_widget
def test_motor_map_init(bec_figure):
def test_motor_map_init(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
default_config = MotorMapConfig(widget_class="BECMotorMap")
mm = bec_figure.motor_map(config=default_config.model_dump())
@@ -18,7 +20,8 @@ def test_motor_map_init(bec_figure):
assert mm.config == default_config
def test_motor_map_change_motors(bec_figure):
def test_motor_map_change_motors(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
assert mm.motor_x == "samx"
@@ -32,7 +35,8 @@ def test_motor_map_change_motors(bec_figure):
assert mm.config.signals.y == SignalData(name="samz", entry="samz", limits=[-8, 8])
def test_motor_map_get_limits(bec_figure):
def test_motor_map_get_limits(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
expected_limits = {"samx": [-10, 10], "samy": [-5, 5]}
@@ -41,7 +45,8 @@ def test_motor_map_get_limits(bec_figure):
assert actual_limit == expected_limit
def test_motor_map_get_init_position(bec_figure):
def test_motor_map_get_init_position(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
mm.set_precision(2)
@@ -57,7 +62,8 @@ def test_motor_map_get_init_position(bec_figure):
assert actual_position == expected_position
def test_motor_movement_updates_position_and_database(bec_figure):
def test_motor_movement_updates_position_and_database(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
motor_map_dev = mm.client.device_manager.devices
@@ -85,7 +91,8 @@ def test_motor_movement_updates_position_and_database(bec_figure):
assert mm.database_buffer["y"] == init_positions["samy"]
def test_scatter_plot_rendering(bec_figure):
def test_scatter_plot_rendering(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
motor_map_dev = mm.client.device_manager.devices
@@ -115,7 +122,8 @@ def test_scatter_plot_rendering(bec_figure):
), "Scatter plot Y data should retain last known position"
def test_plot_visualization_consistency(bec_figure):
def test_plot_visualization_consistency(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
mm.change_motors("samx", "samy")
# Simulate updating the plot with new data
@@ -133,7 +141,8 @@ def test_plot_visualization_consistency(bec_figure):
), "Plot not updated correctly with new data"
def test_change_background_value(bec_figure, qtbot):
def test_change_background_value(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
assert mm.config.background_value == 25
@@ -146,7 +155,8 @@ def test_change_background_value(bec_figure, qtbot):
assert np.all(mm.plot_components["limit_map"].image == 50.0)
def test_motor_map_init_from_config(bec_figure):
def test_motor_map_init_from_config(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
config = {
"widget_class": "BECMotorMap",
"gui_id": "mm_id",
@@ -200,7 +210,8 @@ def test_motor_map_init_from_config(bec_figure):
assert mm._config_dict == config
def test_motor_map_set_scatter_size(bec_figure, qtbot):
def test_motor_map_set_scatter_size(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
assert mm.config.scatter_size == 5
@@ -213,7 +224,8 @@ def test_motor_map_set_scatter_size(bec_figure, qtbot):
assert mm.plot_components["scatter"].opts["size"] == 10
def test_motor_map_change_precision(bec_figure):
def test_motor_map_change_precision(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
assert mm.config.precision == 2
@@ -221,7 +233,8 @@ def test_motor_map_change_precision(bec_figure):
assert mm.config.precision == 10
def test_motor_map_set_color(bec_figure, qtbot):
def test_motor_map_set_color(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
assert mm.config.color == (255, 255, 255, 255)
@@ -231,7 +244,8 @@ def test_motor_map_set_color(bec_figure, qtbot):
assert mm.config.color == (0, 0, 0, 255)
def test_motor_map_get_data_max_points(bec_figure, qtbot):
def test_motor_map_get_data_max_points(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
mm = bec_figure.motor_map("samx", "samy")
motor_map_dev = mm.client.device_manager.devices

View File

@@ -93,7 +93,6 @@ def bec_queue(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_bec_queue(bec_queue, bec_queue_msg_full):

View File

@@ -20,7 +20,6 @@ def status_box(qtbot, mocked_client, service_status_fixture):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_update_top_item(status_box):

View File

@@ -10,7 +10,6 @@ def color_map_selector(qtbot):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_color_map_selector_init(color_map_selector):

View File

@@ -1,98 +0,0 @@
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

@@ -19,7 +19,6 @@ def device_input_base(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_device_input_base_init(device_input_base):

View File

@@ -12,7 +12,6 @@ def device_input_combobox(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.fixture
@@ -28,7 +27,6 @@ def device_input_combobox_with_config(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.fixture
@@ -43,7 +41,6 @@ def device_input_combobox_with_kwargs(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_device_input_combobox_init(device_input_combobox):
@@ -101,7 +98,6 @@ def device_input_line_edit(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.fixture
@@ -117,7 +113,6 @@ def device_input_line_edit_with_config(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.fixture
@@ -132,7 +127,6 @@ def device_input_line_edit_with_kwargs(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_device_input_line_edit_init(device_input_line_edit):

View File

@@ -16,7 +16,6 @@ def motor_map_widget(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.fixture
@@ -139,7 +138,6 @@ def motor_map_settings(qtbot):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_display_current_settings(motor_map_settings):

View File

@@ -4,18 +4,22 @@ from unittest import mock
import pytest
from qtpy.QtGui import QFontInfo
from bec_widgets.widgets.figure import BECFigure
from .client_mocks import mocked_client
from .test_bec_figure import bec_figure
from .conftest import create_widget
def test_init_plot_base(bec_figure):
def test_init_plot_base(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
assert plot_base is not None
assert plot_base.config.widget_class == "BECPlotBase"
assert plot_base.config.gui_id == "test_plot"
def test_plot_base_axes_by_separate_methods(bec_figure):
def test_plot_base_axes_by_separate_methods(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
plot_base.set_title("Test Title")
@@ -65,7 +69,8 @@ def test_plot_base_axes_by_separate_methods(bec_figure):
assert mock_set_title.call_args == call
def test_plot_base_axes_added_by_kwargs(bec_figure):
def test_plot_base_axes_added_by_kwargs(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
plot_base.set(

View File

@@ -0,0 +1,130 @@
from unittest import mock
import pytest
from bec_lib.device import Positioner
from bec_lib.endpoints import MessageEndpoints
from bec_lib.messages import ScanQueueMessage
from qtpy.QtGui import QValidator
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
from bec_widgets.widgets.positioner_box.positioner_control_line import PositionerControlLine
from .client_mocks import mocked_client
@pytest.fixture
def positioner_box(qtbot, mocked_client):
"""Fixture for PositionerBox widget"""
with mock.patch("bec_widgets.widgets.positioner_box.positioner_box.uuid.uuid4") as mock_uuid:
mock_uuid.return_value = "fake_uuid"
with mock.patch(
"bec_widgets.widgets.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
return_value=True,
):
db = PositionerBox(device="samx", client=mocked_client)
qtbot.addWidget(db)
yield db
def test_positioner_box(positioner_box):
"""Test init of positioner box"""
assert positioner_box.device == "samx"
data = positioner_box.dev["samx"].read()
# Avoid check for Positioner class from BEC in _init_device
setpoint_text = positioner_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 isnit
precision = positioner_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 positioner_box.ui.step_size.value() == 10**-precision * 10
def test_positioner_box_update_limits(positioner_box):
"""Test update of limits"""
positioner_box._limits = None
positioner_box.update_limits([0, 10])
assert positioner_box._limits == [0, 10]
assert positioner_box.setpoint_validator.bottom() == 0
assert positioner_box.setpoint_validator.top() == 10
assert positioner_box.setpoint_validator.validate("100", 0) == (
QValidator.State.Intermediate,
"100",
0,
)
positioner_box.update_limits(None)
assert positioner_box._limits is None
assert positioner_box.setpoint_validator.validate("100", 0) == (
QValidator.State.Acceptable,
"100",
0,
)
def test_positioner_box_on_stop(positioner_box):
"""Test on stop button"""
with mock.patch.object(positioner_box.client.connector, "send") as mock_send:
positioner_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_positioner_box_setpoint_change(positioner_box):
"""Test positioner box setpoint change"""
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.setpoint.setText("100")
positioner_box.on_setpoint_change()
mock_move.assert_called_once_with(100, relative=False)
def test_positioner_box_on_tweak_right(positioner_box):
"""Test tweak right button"""
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.step_size.setValue(0.1)
positioner_box.on_tweak_right()
mock_move.assert_called_once_with(0.1, relative=True)
def test_positioner_box_on_tweak_left(positioner_box):
"""Test tweak left button"""
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.step_size.setValue(0.1)
positioner_box.on_tweak_left()
mock_move.assert_called_once_with(-0.1, relative=True)
def test_positioner_box_setpoint_out_of_range(positioner_box):
"""Test setpoint out of range"""
positioner_box.update_limits([0, 10])
positioner_box.ui.setpoint.setText("100")
positioner_box.on_setpoint_change()
assert positioner_box.ui.setpoint.text() == "100"
assert positioner_box.ui.setpoint.hasAcceptableInput() == False
def test_positioner_control_line(qtbot, mocked_client):
"""Test PositionerControlLine.
Inherits from PositionerBox, but the layout is changed. Check dimensions only
"""
with mock.patch("bec_widgets.widgets.positioner_box.positioner_box.uuid.uuid4") as mock_uuid:
mock_uuid.return_value = "fake_uuid"
with mock.patch(
"bec_widgets.widgets.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
return_value=True,
):
db = PositionerControlLine(device="samx", client=mocked_client)
qtbot.addWidget(db)
assert db.ui.device_box.height() == 60
assert db.ui.device_box.width() == 600

View File

@@ -18,7 +18,6 @@ def ring_progress_bar(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_bar_init(ring_progress_bar):

View File

@@ -220,7 +220,6 @@ def scan_control(qtbot, mocked_client): # , mock_dev):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_populate_scans(scan_control, mocked_client):

View File

@@ -16,7 +16,6 @@ def setting_widget(qtbot):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_setting_widget_initialization(setting_widget):
@@ -61,8 +60,8 @@ def settings_dialog(qtbot, setting_widget):
qtbot.addWidget(dialog)
qtbot.waitExposed(dialog)
yield dialog, parent_widget, setting_widget
dialog.close()
parent_widget.close()
parent_widget.deleteLater()
def test_settings_dialog_initialization(settings_dialog):

View File

@@ -13,7 +13,6 @@ def stop_button(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_stop_button(stop_button):

View File

@@ -14,7 +14,6 @@ def text_box_widget(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_textbox_widget(text_box_widget):

View File

@@ -10,7 +10,6 @@ def toggle(qtbot):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_toggle(toggle):

View File

@@ -14,9 +14,9 @@ from .client_mocks import mocked_client
def vscode_widget(qtbot, mocked_client):
with mock.patch("bec_widgets.widgets.vscode.vscode.subprocess.Popen") as mock_popen:
widget = VSCodeEditor(client=mocked_client)
# qtbot.addWidget(widget)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
# widget.close()
def test_vscode_widget(qtbot, vscode_widget):

View File

@@ -4,13 +4,15 @@ from unittest import mock
import numpy as np
import pytest
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import CurveConfig, Signal, SignalData
from .client_mocks import mocked_client
from .test_bec_figure import bec_figure
from .conftest import create_widget
def test_adding_curve_to_waveform(bec_figure):
def test_adding_curve_to_waveform(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
# adding curve which is in bec - only names
@@ -38,7 +40,8 @@ def test_adding_curve_to_waveform(bec_figure):
assert c3.config.label == "non_existent_device-non_existent_device"
def test_adding_curve_with_same_id(bec_figure):
def test_adding_curve_with_same_id(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i", gui_id="test_curve")
@@ -47,7 +50,8 @@ def test_adding_curve_with_same_id(bec_figure):
assert "Curve with ID 'test_curve' already exists." in str(excinfo.value)
def test_create_waveform1D_by_config(bec_figure):
def test_create_waveform1D_by_config(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1_config_input = {
"widget_class": "BECWaveform",
"gui_id": "widget_1",
@@ -132,7 +136,8 @@ def test_create_waveform1D_by_config(bec_figure):
assert w1.config.axis.title == "Widget 1"
def test_change_gui_id(bec_figure):
def test_change_gui_id(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
w1.change_gui_id("new_id")
@@ -141,7 +146,8 @@ def test_change_gui_id(bec_figure):
assert c1.config.parent_id == "new_id"
def test_getting_curve(bec_figure):
def test_getting_curve(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i", gui_id="test_curve")
c1_expected_config = CurveConfig(
@@ -173,7 +179,8 @@ def test_getting_curve(bec_figure):
assert c1.get_config() == c1_expected_config.model_dump()
def test_getting_curve_errors(bec_figure):
def test_getting_curve_errors(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i", gui_id="test_curve")
@@ -190,7 +197,8 @@ def test_getting_curve_errors(bec_figure):
)
def test_add_curve(bec_figure):
def test_add_curve(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
@@ -201,7 +209,8 @@ def test_add_curve(bec_figure):
assert c1.config.source == "scan_segment"
def test_change_legend_font_size(bec_figure):
def test_change_legend_font_size(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
plot = bec_figure.plot()
w1 = plot.add_curve_bec(x_name="samx", y_name="bpm4i")
@@ -213,7 +222,8 @@ def test_change_legend_font_size(bec_figure):
assert mock_set_scale.call_args == mock.call(2)
def test_remove_curve(bec_figure):
def test_remove_curve(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
w1.add_curve_bec(x_name="samx", y_name="bpm4i")
@@ -231,7 +241,8 @@ def test_remove_curve(bec_figure):
)
def test_change_curve_appearance_methods(bec_figure, qtbot):
def test_change_curve_appearance_methods(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
@@ -260,7 +271,8 @@ def test_change_curve_appearance_methods(bec_figure, qtbot):
}
def test_change_curve_appearance_args(bec_figure):
def test_change_curve_appearance_args(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
@@ -290,7 +302,8 @@ def test_change_curve_appearance_args(bec_figure):
}
def test_set_custom_curve_data(bec_figure, qtbot):
def test_set_custom_curve_data(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_custom(
@@ -326,7 +339,8 @@ def test_set_custom_curve_data(bec_figure, qtbot):
assert np.array_equal(y_new, [7, 8, 9])
def test_custom_data_2D_array(bec_figure, qtbot):
def test_custom_data_2D_array(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
data = np.random.rand(10, 2)
@@ -338,7 +352,8 @@ def test_custom_data_2D_array(bec_figure, qtbot):
assert np.array_equal(y, data[:, 1])
def test_get_all_data(bec_figure):
def test_get_all_data(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_custom(
@@ -373,7 +388,8 @@ def test_get_all_data(bec_figure):
}
def test_curve_add_by_config(bec_figure):
def test_curve_add_by_config(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1_config_input = {
@@ -413,7 +429,8 @@ def test_curve_add_by_config(bec_figure):
assert c1.get_config(False) == CurveConfig(**c1_config_input)
def test_scan_update(bec_figure, qtbot):
def test_scan_update(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
@@ -447,7 +464,8 @@ def test_scan_update(bec_figure, qtbot):
assert c1.get_data() == ([10], [5])
def test_scan_history_with_val_access(bec_figure, qtbot):
def test_scan_history_with_val_access(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
w1.plot(x_name="samx", y_name="bpm4i")
@@ -472,7 +490,8 @@ def test_scan_history_with_val_access(bec_figure, qtbot):
assert np.array_equal(y_data, [4, 5, 6])
def test_scatter_2d_update(bec_figure, qtbot):
def test_scatter_2d_update(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
c1 = w1.add_curve_bec(x_name="samx", y_name="samx", z_name="bpm4i")
@@ -512,7 +531,8 @@ def test_scatter_2d_update(bec_figure, qtbot):
assert colors == expected_z_colors
def test_waveform_single_arg_inputs(bec_figure, qtbot):
def test_waveform_single_arg_inputs(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
w1.plot("bpm4i")
@@ -544,7 +564,8 @@ def test_waveform_single_arg_inputs(bec_figure, qtbot):
assert np.array_equal(w1._curves_data["custom"]["np_ndarray 2D"].get_data(), data_array_2D.T)
def test_waveform_set_x_sync(bec_figure, qtbot):
def test_waveform_set_x_sync(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot()
custom_label = "custom_label"
w1.plot("bpm4i")
@@ -601,7 +622,8 @@ def test_waveform_set_x_sync(bec_figure, qtbot):
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [timestamp]"
def test_waveform_async_data_update(bec_figure, qtbot):
def test_waveform_async_data_update(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot("async_device")
custom_label = "custom_label"
w1.set_x_label(custom_label)
@@ -647,7 +669,8 @@ def test_waveform_async_data_update(bec_figure, qtbot):
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [best_effort]"
def test_waveform_set_x_async(bec_figure, qtbot):
def test_waveform_set_x_async(qtbot, mocked_client):
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
w1 = bec_figure.plot("async_device")
custom_label = "custom_label"
w1.set_x_label(custom_label)

View File

@@ -3,6 +3,9 @@ from unittest.mock import MagicMock, patch
import pyqtgraph as pg
import pytest
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
from bec_widgets.widgets.waveform.waveform_popups.curve_dialog.curve_dialog import CurveSettings
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
from .client_mocks import mocked_client
@@ -14,7 +17,6 @@ def waveform_widget(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.fixture
@@ -262,3 +264,199 @@ def test_enable_mouse_pan_mode(qtbot, waveform_widget):
assert action_drag.isChecked() == True
assert action_rectangle.isChecked() == False
mock_view_box.setMouseMode.assert_called_once_with(pg.ViewBox.PanMode)
###################################
# Curve Dialog Tests
###################################
def show_curve_dialog(qtbot, waveform_widget):
curve_dialog = SettingsDialog(
waveform_widget,
settings_widget=CurveSettings(),
window_title="Curve Settings",
config=waveform_widget.waveform._curves_data,
)
qtbot.addWidget(curve_dialog)
qtbot.waitExposed(curve_dialog)
return curve_dialog
def test_curve_dialog_scan_curves_interactions(qtbot, waveform_widget):
waveform_widget.plot(y_name="bpm4i")
waveform_widget.plot(y_name="bpm3a")
curve_dialog = show_curve_dialog(qtbot, waveform_widget)
# Check default display of config from waveform widget
assert curve_dialog is not None
assert curve_dialog.widget.ui.scan_table.rowCount() == 2
assert curve_dialog.widget.ui.scan_table.cellWidget(0, 0).text() == "bpm4i"
assert curve_dialog.widget.ui.scan_table.cellWidget(0, 1).text() == "bpm4i"
assert curve_dialog.widget.ui.scan_table.cellWidget(1, 0).text() == "bpm3a"
assert curve_dialog.widget.ui.scan_table.cellWidget(1, 1).text() == "bpm3a"
assert curve_dialog.widget.ui.x_mode.currentText() == "best_effort"
assert curve_dialog.widget.ui.x_name.isEnabled() == False
assert curve_dialog.widget.ui.x_entry.isEnabled() == False
# Add a new curve
curve_dialog.widget.ui.add_curve.click()
qtbot.wait(200)
assert curve_dialog.widget.ui.scan_table.rowCount() == 3
# Set device to new curve
curve_dialog.widget.ui.scan_table.cellWidget(2, 0).setText("bpm3i")
# Change the x mode to device
curve_dialog.widget.ui.x_mode.setCurrentText("device")
qtbot.wait(200)
assert curve_dialog.widget.ui.x_name.isEnabled() == True
assert curve_dialog.widget.ui.x_entry.isEnabled() == True
# Set the x device
curve_dialog.widget.ui.x_name.setText("samx")
# Delete first curve ('bpm4i')
curve_dialog.widget.ui.scan_table.cellWidget(0, 6).click()
qtbot.wait(200)
assert curve_dialog.widget.ui.scan_table.rowCount() == 2
assert curve_dialog.widget.ui.scan_table.cellWidget(0, 0).text() == "bpm3a"
assert curve_dialog.widget.ui.scan_table.cellWidget(0, 1).text() == "bpm3a"
assert curve_dialog.widget.ui.scan_table.cellWidget(1, 0).text() == "bpm3i"
# Close the dialog
curve_dialog.accept()
qtbot.wait(200)
# Check the curve data in the target widget
assert list(waveform_widget.waveform._curves_data["scan_segment"].keys()) == [
"bpm3a-bpm3a",
"bpm3i-bpm3i",
]
assert len(waveform_widget.curves) == 2
def test_curve_dialog_async(qtbot, waveform_widget):
waveform_widget.plot(y_name="bpm4i")
waveform_widget.plot(y_name="async_device")
curve_dialog = show_curve_dialog(qtbot, waveform_widget)
assert curve_dialog is not None
assert curve_dialog.widget.ui.scan_table.rowCount() == 2
assert curve_dialog.widget.ui.scan_table.cellWidget(0, 0).text() == "bpm4i"
assert curve_dialog.widget.ui.scan_table.cellWidget(0, 1).text() == "bpm4i"
assert curve_dialog.widget.ui.scan_table.cellWidget(1, 0).text() == "async_device"
assert curve_dialog.widget.ui.scan_table.cellWidget(1, 1).text() == "async_device"
def test_curve_dialog_dap(qtbot, waveform_widget):
waveform_widget.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
curve_dialog = show_curve_dialog(qtbot, waveform_widget)
assert curve_dialog is not None
assert curve_dialog.widget.ui.scan_table.rowCount() == 1
assert curve_dialog.widget.ui.scan_table.cellWidget(0, 0).text() == "bpm4i"
assert curve_dialog.widget.ui.scan_table.cellWidget(0, 1).text() == "bpm4i"
assert curve_dialog.widget.ui.dap_table.isEnabled() == True
assert curve_dialog.widget.ui.dap_table.rowCount() == 1
assert curve_dialog.widget.ui.dap_table.cellWidget(0, 0).text() == "bpm4i"
assert curve_dialog.widget.ui.dap_table.cellWidget(0, 1).text() == "bpm4i"
assert curve_dialog.widget.ui.x_mode.currentText() == "device"
assert curve_dialog.widget.ui.x_name.isEnabled() == True
assert curve_dialog.widget.ui.x_entry.isEnabled() == True
assert curve_dialog.widget.ui.x_name.text() == "samx"
assert curve_dialog.widget.ui.x_entry.text() == "samx"
curve_dialog.accept()
qtbot.wait(200)
assert list(waveform_widget.waveform._curves_data["scan_segment"].keys()) == ["bpm4i-bpm4i"]
assert len(waveform_widget.curves) == 2
###################################
# Axis Dialog Tests
###################################
def show_axis_dialog(qtbot, waveform_widget):
axis_dialog = SettingsDialog(
waveform_widget,
settings_widget=AxisSettings(),
window_title="Axis Settings",
config=waveform_widget._config_dict["axis"],
)
qtbot.addWidget(axis_dialog)
qtbot.waitExposed(axis_dialog)
return axis_dialog
def test_axis_dialog_with_axis_limits(qtbot, waveform_widget):
waveform_widget.set(
title="Test Title",
x_label="X Label",
y_label="Y Label",
x_scale="linear",
y_scale="log",
x_lim=(0, 10),
y_lim=(0, 10),
)
axis_dialog = show_axis_dialog(qtbot, waveform_widget)
assert axis_dialog is not None
assert axis_dialog.widget.ui.plot_title.text() == "Test Title"
assert axis_dialog.widget.ui.x_label.text() == "X Label"
assert axis_dialog.widget.ui.y_label.text() == "Y Label"
assert axis_dialog.widget.ui.x_scale.currentText() == "linear"
assert axis_dialog.widget.ui.y_scale.currentText() == "log"
assert axis_dialog.widget.ui.x_min.value() == 0
assert axis_dialog.widget.ui.x_max.value() == 10
assert axis_dialog.widget.ui.y_min.value() == 0
assert axis_dialog.widget.ui.y_max.value() == 10
def test_axis_dialog_without_axis_limits(qtbot, waveform_widget):
waveform_widget.set(
title="Test Title", x_label="X Label", y_label="Y Label", x_scale="linear", y_scale="log"
)
x_range = waveform_widget.fig.widget_list[0].plot_item.viewRange()[0]
y_range = waveform_widget.fig.widget_list[0].plot_item.viewRange()[1]
axis_dialog = show_axis_dialog(qtbot, waveform_widget)
assert axis_dialog is not None
assert axis_dialog.widget.ui.plot_title.text() == "Test Title"
assert axis_dialog.widget.ui.x_label.text() == "X Label"
assert axis_dialog.widget.ui.y_label.text() == "Y Label"
assert axis_dialog.widget.ui.x_scale.currentText() == "linear"
assert axis_dialog.widget.ui.y_scale.currentText() == "log"
assert axis_dialog.widget.ui.x_min.value() == x_range[0]
assert axis_dialog.widget.ui.x_max.value() == x_range[1]
assert axis_dialog.widget.ui.y_min.value() == y_range[0]
assert axis_dialog.widget.ui.y_max.value() == y_range[1]
def test_axis_dialog_set_properties(qtbot, waveform_widget):
axis_dialog = show_axis_dialog(qtbot, waveform_widget)
axis_dialog.widget.ui.plot_title.setText("New Title")
axis_dialog.widget.ui.x_label.setText("New X Label")
axis_dialog.widget.ui.y_label.setText("New Y Label")
axis_dialog.widget.ui.x_scale.setCurrentText("log")
axis_dialog.widget.ui.y_scale.setCurrentText("linear")
axis_dialog.widget.ui.x_min.setValue(5)
axis_dialog.widget.ui.x_max.setValue(15)
axis_dialog.widget.ui.y_min.setValue(5)
axis_dialog.widget.ui.y_max.setValue(15)
axis_dialog.accept()
assert waveform_widget._config_dict["axis"]["title"] == "New Title"
assert waveform_widget._config_dict["axis"]["x_label"] == "New X Label"
assert waveform_widget._config_dict["axis"]["y_label"] == "New Y Label"
assert waveform_widget._config_dict["axis"]["x_scale"] == "log"
assert waveform_widget._config_dict["axis"]["y_scale"] == "linear"
assert waveform_widget._config_dict["axis"]["x_lim"] == (5, 15)
assert waveform_widget._config_dict["axis"]["y_lim"] == (5, 15)

View File

@@ -12,8 +12,6 @@ def website_widget(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.page().deleteLater()
qtbot.wait(1000)
def test_website_widget_set_url(website_widget):