mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-09 18:20:55 +02:00
Compare commits
40 Commits
refactor/b
...
v0.94.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49a5a23d41 | ||
| 4f96d0e4a1 | |||
| ea9240d2f7 | |||
| 4d02b42f11 | |||
|
|
9509be14be | ||
| 198c1d1064 | |||
| 2af5c94913 | |||
|
|
a4a0bac3c1 | ||
| f285b35b49 | |||
| 7aeb2b5c26 | |||
| d56ea95ef9 | |||
|
|
5733fea98c | ||
| 98b79aac7b | |||
|
|
4212fe0e32 | ||
| 93d397759c | |||
|
|
8c5b901a37 | ||
| 0273bf4856 | |||
| c80a7cd108 | |||
|
|
a50d9c7b3f | ||
| 281633deff | |||
| 0d190c5c59 | |||
| 6269009e54 | |||
| 6d2442d23c | |||
|
|
110b27351b | ||
| 37aa371e7c | |||
| eb54e9f788 | |||
|
|
482efeb340 | ||
| 99ee545e41 | |||
| cf94599c25 | |||
| b50b3a27e6 | |||
| bf6294ecbf | |||
| a3d4f5ac4b | |||
| bc264975b1 | |||
| ad07bbf85e | |||
| 9856857f4c | |||
| f9e5897900 | |||
|
|
39fb22b716 | ||
| a372925fff | |||
|
|
ec54440569 | ||
| af86860bf3 |
242
CHANGELOG.md
242
CHANGELOG.md
@@ -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))
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
3
bec_widgets/assets/toolbar_icons/device_line_edit.svg
Normal file
3
bec_widgets/assets/toolbar_icons/device_line_edit.svg
Normal 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 |
|
Before Width: | Height: | Size: 266 B After Width: | Height: | Size: 266 B |
@@ -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()
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
47
bec_widgets/qt_utils/redis_message_waiter.py
Normal file
47
bec_widgets/qt_utils/redis_message_waiter.py
Normal 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, {})
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['device_box.py']}
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['positioner_box.py']}
|
||||
@@ -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">
|
||||
@@ -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()
|
||||
@@ -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_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['positioner_control_line.py']}
|
||||
216
bec_widgets/widgets/positioner_box/positioner_control_line.ui
Normal file
216
bec_widgets/widgets/positioner_box/positioner_control_line.ui
Normal 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>
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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__(
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -561,7 +561,6 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
|
||||
def cleanup(self):
|
||||
self.fig.cleanup()
|
||||
self.client.shutdown()
|
||||
return super().cleanup()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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/
|
||||
```
|
||||
14
docs/developer/introduction/concepts.md
Normal file
14
docs/developer/introduction/concepts.md
Normal 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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
15
docs/developer/introduction/introduction.md
Normal file
15
docs/developer/introduction/introduction.md
Normal 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/
|
||||
```
|
||||
@@ -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}
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -15,7 +15,6 @@ def image_widget(qtbot, mocked_client):
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
130
tests/unit_tests/test_positioner_box.py
Normal file
130
tests/unit_tests/test_positioner_box.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -10,7 +10,6 @@ def toggle(qtbot):
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def test_toggle(toggle):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user