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

Compare commits

...

46 Commits

Author SHA1 Message Date
semantic-release
110b27351b 0.93.4
Automatically generated by python-semantic-release
2024-08-07 16:21:43 +00:00
37aa371e7c fix: rename DeviceBox to PositionerBox, fix test for validation 2024-08-07 17:56:48 +02:00
eb54e9f788 fix: add validation for bec_lib.device.Positioner; closes #268 2024-08-07 15:45:39 +02:00
semantic-release
482efeb340 0.93.3
Automatically generated by python-semantic-release
2024-08-07 13:14:54 +00:00
99ee545e41 fix(dock): properly shut down docks and temp areas 2024-08-07 13:58:43 +02:00
cf94599c25 test: removed quit from teardown 2024-08-07 12:25:54 +02:00
b50b3a27e6 fix(settings): shut down settings dialog 2024-08-07 12:25:54 +02:00
bf6294ecbf test: removed explicit call to close the widget 2024-08-07 12:25:54 +02:00
a3d4f5ac4b fix(website): fixed teardown of website widgets 2024-08-07 11:15:14 +02:00
bc264975b1 fix(dock): properly shut down docks and dock areas 2024-08-07 11:00:25 +02:00
ad07bbf85e fix(figure): cleanup pyqtgraph 2024-08-07 10:12:49 +02:00
9856857f4c test: use factory instead of fixture to properly cleanup widgets on teardown 2024-08-07 10:12:49 +02:00
f9e5897900 test: ensure all toplevelwidgets are closed 2024-08-07 10:12:49 +02:00
semantic-release
39fb22b716 0.93.2
Automatically generated by python-semantic-release
2024-08-07 07:57:18 +00:00
a372925fff fix(scan_group_box): Scan Spinboxes limits increased to max allowed values; setting dialog for step size and decimal precision for ScanDoubleSpinBox on right click 2024-08-07 09:47:06 +02:00
semantic-release
ec54440569 0.93.1
Automatically generated by python-semantic-release
2024-08-06 21:56:54 +00:00
af86860bf3 fix(dock): docks have more recognizable red icon for closing docks 2024-08-06 19:23:31 +02:00
302ae90139 docs: added video tutorial section with BSEG YT video 2024-08-06 17:42:15 +02:00
semantic-release
1405068925 0.93.0
Automatically generated by python-semantic-release
2024-08-05 14:11:40 +00:00
5aad401ef8 feat(themes): moved themes to bec_qthemes
This reverts commit fd6ae91993
2024-08-05 14:24:05 +02:00
semantic-release
885dcfda89 0.92.5
Automatically generated by python-semantic-release
2024-08-05 12:20:01 +00:00
30fef929cf fix(spinner): stop timer on close event 2024-08-05 13:54:20 +02:00
1f30dd73a9 fix(status_box): fix cleanup of status box 2024-08-05 13:54:20 +02:00
73cd11e472 test: register all widgets with qtbot and close them 2024-08-05 13:54:20 +02:00
7616ca0e14 refactor(queue): refactored bec queue to inherit only from qwidget 2024-08-05 13:54:20 +02:00
semantic-release
ca29a69779 0.92.4
Automatically generated by python-semantic-release
2024-07-31 07:24:44 +00:00
dcc5fd71ee fix: fix missmatch of signal/slot in image and motormap 2024-07-29 16:05:21 +02:00
semantic-release
fee4901657 0.92.3
Automatically generated by python-semantic-release
2024-07-28 10:05:23 +00:00
71873ddf35 fix(docs): moved to pyside6 2024-07-28 11:17:17 +02:00
semantic-release
f8552ca551 0.92.2
Automatically generated by python-semantic-release
2024-07-28 08:53:31 +00:00
995a795060 fix(widgets): fixed import for tictactoe example 2024-07-28 10:42:32 +02:00
semantic-release
7ab81c5797 0.92.1
Automatically generated by python-semantic-release
2024-07-28 07:04:29 +00:00
bc1e23944c fix: use SafeSlot instead of Slot 2024-07-28 08:54:24 +02:00
a3fe20500a fix: linting 2024-07-28 08:54:24 +02:00
61a4e32deb fix: always add a QApplication for tests 2024-07-28 08:54:24 +02:00
3d681f77e1 fix: add xvfb to draw offscreen 2024-07-28 08:54:24 +02:00
5a9ccfd1f6 fix: reset ErrorPopup singleton between tests 2024-07-26 11:58:07 +02:00
fc57b7a126 fix: metaclass + QObject segfaults PyQt(cpp bindings) 2024-07-26 11:58:07 +02:00
06205e0790 build(ci): install ophyd_devices in editable mode for pipelines 2024-07-25 09:46:58 +02:00
4be6fd6b83 refactor: renamed DeviceMonitor2DMessage 2024-07-25 09:46:58 +02:00
714e1e139e refactor: rename device_monitor to device_monitor_2d 2024-07-25 09:46:58 +02:00
semantic-release
01c0e0b1df 0.92.0
Automatically generated by python-semantic-release
2024-07-24 18:52:56 +00:00
4457ef2147 fix(dock): custom label can be created closable 2024-07-23 22:22:16 +02:00
8ca60d54b3 feat(dock): dock style sheets updated 2024-07-23 22:22:16 +02:00
5696c993dc feat(general_gui): general gui added 2024-07-23 22:22:16 +02:00
1206e15309 fix(device_combobox): set minimum size to 125px 2024-07-23 22:22:16 +02:00
77 changed files with 1347 additions and 558 deletions

View File

@@ -43,13 +43,20 @@ stages:
- export QTWEBENGINE_DISABLE_SANDBOX=1
.clone-repos: &clone-repos
- echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
.install-repos: &install-repos
- pip install -e ./ophyd_devices
- pip install -e ./bec/bec_lib[dev]
- pip install -e ./bec/bec_ipython_client
.install-os-packages: &install-os-packages
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
- *install-qt-webengine-deps
before_script:
@@ -131,8 +138,7 @@ tests:
script:
- *clone-repos
- *install-os-packages
- pip install -e ./bec/bec_lib[dev]
- pip install -e ./bec/bec_ipython_client
- *install-repos
- pip install -e .[dev,pyqt6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
@@ -169,8 +175,7 @@ test-matrix:
script:
- *clone-repos
- *install-os-packages
- pip install -e ./bec/bec_lib[dev]
- pip install -e ./bec/bec_ipython_client
- *install-repos
- pip install -e .[dev,$QT_PCKG]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
allow_failure: true
@@ -195,10 +200,9 @@ end-2-end-conda:
- cd ./bec
- source ./bin/install_bec_dev.sh -t
- pip install -e ./bec_lib[dev]
- pip install -e ./bec_ipython_client[dev]
- cd ../
- pip install -e ./ophyd_devices
- pip install -e .[dev,pyqt6]
- cd ./tests/end-2-end
- pytest -v --start-servers --flush-redis --random-order

View File

@@ -1,15 +1,141 @@
# CHANGELOG
## 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
* feat(themes): moved themes to bec_qthemes
This reverts commit fd6ae91993a23a7b8dbb2cf3c4b7c3eda6d2b0f6 ([`5aad401`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5aad401ef8774c7330784f72cd3b9d8c253e2b6a))
## v0.92.5 (2024-08-05)
### Fix
* fix(spinner): stop timer on close event ([`30fef92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/30fef929cf6fb4b73f48151c92a0ee54c734031d))
* fix(status_box): fix cleanup of status box ([`1f30dd7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f30dd73a9c1e3135087a5eef92c7329f54a604e))
### Refactor
* refactor(queue): refactored bec queue to inherit only from qwidget ([`7616ca0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7616ca0e145e233ccb48029a8c0b54b54b5b4194))
### Test
* test: register all widgets with qtbot and close them ([`73cd11e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/73cd11e47277e4437554b785a9551b28a572094f))
## v0.92.4 (2024-07-31)
### Fix
* 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))
@@ -19,131 +145,3 @@
### 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)
### Documentation
* docs: readthedocs icon path fixed ([`2bcaa42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2bcaa4256d6daaefacb3ead8c72458d7b1498e29))
### Fix
* fix(plot_base): set_xy autorange moved to plotbase from waveform ([`a3dff7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3dff7decc16115c12dc6b4ef1572552368da309))
### Refactor
* refactor(toolbar): generalizations of the ToolBarAction ([`ad112d1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ad112d1f08157f6987edd48a0bacf9f669ef1997))
## v0.88.0 (2024-07-19)
### Feature
* feat(waveform_widget): designer plugin added ([`1f8ef52`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f8ef52b606283038052640849094f515a463403))
* feat(waveform_widget): switch between drag and rectangle mode ([`2be009c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2be009c6477ba26c5cfb4d827534c5d5eb428999))
* feat(waveform_widget): autorange button ([`8df6b00`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8df6b003e5c6a942fa2e875d9790e492c087bf26))
* feat(waveform_widget): dap parameter window ([`1e551d6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1e551d6e9696f79ea2e0a179d13a4fc6c2a128b2))
* feat(waveform): export to matplotlib window of current scene ([`8d93405`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d9340539967b06b1e15f21a2106a39d5c740f31))
* feat(figure): export dialog can be launched from CLI and from toolbar ([`6ff6111`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ff611109153b9412dce37c527b19e839d99bba7))
* feat(waveform_widget): added error handle utility ([`a8ff1d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8ff1d4cd09cae5eaeb4bd0ea90fdd102e32f3a3))
* feat(curve_dialog): add DAP functionality ([`e830565`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e8305652fde384da037242cf8f7e3606f22bcfb6))
* feat(curve_dialog): curves can be added ([`c926a75`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c926a75a7927d672c044ea8f68771209ae5accc6))
* feat(waveform_widget): BECWaveformWidget toolbar added import/export config ([`fa9b171`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fa9b17191ddbb4043a658dae9aa0801e1dc22b84))
* feat(waveform_widget): BECWaveformWidget added with toolbar ([`755b394`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/755b394c1c4d7c443c442d89c630d08ce5415554))
### Fix
* fix(waveform_widget): plot API unified with BECFigure ([`2c8764a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2c8764a27de89b39b717032b58465e120ec57fbc))
* fix(colormap_selector): compatibility for PyQt6 when using designer fixed ([`50135b5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/50135b5fe90a88618291e9357f180cb19251dace))
* fix(waveform_widget): adapted for BECWidget base class ([`6eb313f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6eb313fa76e559d62ecd8fa8849142b83817e47c))
* fix(waveform_widget): temporary disabled save/load config ([`7089cf3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7089cf356a43d805241d5621952e544d690e65e0))
* fix(waveform_widget): use @SafeSlot decorator for automatic error message ([`8e588d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e588d79c86e950f6915e89c08fa9415c4bd8033))
* fix(waveform): colormaps of curves can be changed and normalised
feat(waveform): colormap can be changed from curve dialog
fix(curve_dialog): default dialog parameters fixed
curve Dialog colormap WIP ([`33495cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33495cfe03b363f18db61d8af2983f49027b7a43))
* fix(waveform_widget): adapted for changes from improved scan logic from waveform widget ([`8ac35d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ac35d7280b1ff007c10612228d163cc0c5d1a99))
### Refactor
* refactor(icons): icons moved to the assets directory ([`a8b6ef2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8b6ef20cccae87515b10f054d0ed5b10e152769))
* refactor(waveform_widget): removed PYSIDE6 check ([`47fcb9e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/47fcb9ebfe35ae600cced95a1edc68f6f6e37a04))
### Test
* test(waveform_widget): test added ([`8d764e2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d764e2d46a1e017dadc3c4630648c1ca708afc2))
## v0.87.1 (2024-07-18)
### Fix
* fix(dock): added hasattr to cleanup method for widgets ([`d75c55b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d75c55b2b1ccf156fb789c7813f1c5bdf256f860))
* fix: add missing close() call, ensure jupyter console client.shutdown() is called in closeEvent ([`e52ee26`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e52ee2604cb35096f1bd833ca9516d8a34197d35))
### Refactor
* refactor: BECWidget is a mixin based on BECConnector, for each QWidget in BEC
Handles closeEvent() and RPC registering/unregistering ([`c7feb69`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c7feb6952d590b569f7b0cba3b019a9af0ce0c93))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -21,9 +21,9 @@ class Widgets(str, enum.Enum):
BECQueue = "BECQueue"
BECStatusBox = "BECStatusBox"
BECWaveformWidget = "BECWaveformWidget"
DeviceBox = "DeviceBox"
DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit"
PositionerBox = "PositionerBox"
RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl"
StopButton = "StopButton"
@@ -2287,24 +2287,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 +2341,17 @@ 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 Ring(RPCBase):
@rpc_call
def _get_all_rpc(self) -> "dict":

View File

@@ -0,0 +1,92 @@
import os
import sys
from qtpy.QtCore import QSize
from qtpy.QtGui import QActionGroup, QIcon
from qtpy.QtWidgets import QApplication, QMainWindow, QStyle
import bec_widgets
from bec_widgets.examples.general_app.web_links import BECWebLinksMixin
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.ui_loader import UILoader
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class BECGeneralApp(QMainWindow):
def __init__(self, parent=None):
super(BECGeneralApp, self).__init__(parent)
ui_file_path = os.path.join(os.path.dirname(__file__), "general_app.ui")
self.load_ui(ui_file_path)
self.resize(1280, 720)
self.ini_ui()
def ini_ui(self):
self._setup_icons()
self._hook_menubar_docs()
self._hook_theme_bar()
def load_ui(self, ui_file):
loader = UILoader(self)
self.ui = loader.loader(ui_file)
self.setCentralWidget(self.ui)
def _hook_menubar_docs(self):
# BEC Docs
self.ui.action_BEC_docs.triggered.connect(BECWebLinksMixin.open_bec_docs)
# BEC Widgets Docs
self.ui.action_BEC_widgets_docs.triggered.connect(BECWebLinksMixin.open_bec_widgets_docs)
# Bug report
self.ui.action_bug_report.triggered.connect(BECWebLinksMixin.open_bec_bug_report)
def change_theme(self, theme):
apply_theme(theme)
def _setup_icons(self):
help_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxQuestion)
bug_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxInformation)
computer_icon = QIcon.fromTheme("computer")
widget_icon = QIcon(os.path.join(MODULE_PATH, "assets", "designer_icons", "dock_area.png"))
self.ui.action_BEC_docs.setIcon(help_icon)
self.ui.action_BEC_widgets_docs.setIcon(help_icon)
self.ui.action_bug_report.setIcon(bug_icon)
self.ui.central_tab.setTabIcon(0, widget_icon)
self.ui.central_tab.setTabIcon(1, computer_icon)
def _hook_theme_bar(self):
self.ui.action_light.setCheckable(True)
self.ui.action_dark.setCheckable(True)
# Create an action group to make sure only one can be checked at a time
theme_group = QActionGroup(self)
theme_group.addAction(self.ui.action_light)
theme_group.addAction(self.ui.action_dark)
theme_group.setExclusive(True)
# Connect the actions to the theme change method
self.ui.action_light.triggered.connect(lambda: self.change_theme("light"))
self.ui.action_dark.triggered.connect(lambda: self.change_theme("dark"))
self.ui.action_dark.trigger()
def main(): # pragma: no cover
app = QApplication(sys.argv)
icon = QIcon()
icon.addFile(
os.path.join(MODULE_PATH, "assets", "app_icons", "BEC-Dark.png"), size=QSize(48, 48)
)
app.setWindowIcon(icon)
main_window = BECGeneralApp()
main_window.show()
sys.exit(app.exec_())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,262 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1718</width>
<height>1139</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="tabShape">
<enum>QTabWidget::TabShape::Rounded</enum>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTabWidget" name="central_tab">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="dock_area_tab">
<attribute name="title">
<string>Dock Area</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="BECDockArea" name="dock_area"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="vscode_tab">
<attribute name="icon">
<iconset theme="QIcon::ThemeIcon::Computer"/>
</attribute>
<attribute name="title">
<string>Visual Studio Code</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="VSCodeEditor" name="vscode"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1718</width>
<height>31</height>
</rect>
</property>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="action_BEC_docs"/>
<addaction name="action_BEC_widgets_docs"/>
<addaction name="action_bug_report"/>
</widget>
<widget class="QMenu" name="menuTheme">
<property name="title">
<string>Theme</string>
</property>
<addaction name="action_light"/>
<addaction name="action_dark"/>
</widget>
<addaction name="menuTheme"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="dock_scan_control">
<property name="windowTitle">
<string>Scan Control</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_2">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="ScanControl" name="scan_control"/>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dock_status_2">
<property name="windowTitle">
<string>BEC Service Status</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_3">
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="BECStatusBox" name="bec_status_box_2"/>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dock_queue">
<property name="windowTitle">
<string>Scan Queue</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_4">
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="BECQueue" name="bec_queue">
<row/>
<column/>
<column/>
<column/>
<item row="0" column="0"/>
<item row="0" column="1"/>
<item row="0" column="2"/>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="action_BEC_docs">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
</property>
<property name="text">
<string>BEC Docs</string>
</property>
</action>
<action name="action_BEC_widgets_docs">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
</property>
<property name="text">
<string>BEC Widgets Docs</string>
</property>
</action>
<action name="action_bug_report">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogError"/>
</property>
<property name="text">
<string>Bug Report</string>
</property>
</action>
<action name="action_light">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Light</string>
</property>
</action>
<action name="action_dark">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Dark</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>WebsiteWidget</class>
<extends>QWebEngineView</extends>
<header>website_widget</header>
</customwidget>
<customwidget>
<class>BECQueue</class>
<extends>QTableWidget</extends>
<header>bec_queue</header>
</customwidget>
<customwidget>
<class>ScanControl</class>
<extends>QWidget</extends>
<header>scan_control</header>
</customwidget>
<customwidget>
<class>VSCodeEditor</class>
<extends>WebsiteWidget</extends>
<header>vs_code_editor</header>
</customwidget>
<customwidget>
<class>BECStatusBox</class>
<extends>QWidget</extends>
<header>bec_status_box</header>
</customwidget>
<customwidget>
<class>BECDockArea</class>
<extends>QWidget</extends>
<header>dock_area</header>
</customwidget>
<customwidget>
<class>QWebEngineView</class>
<extends></extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,15 @@
import webbrowser
class BECWebLinksMixin:
@staticmethod
def open_bec_docs():
webbrowser.open("https://beamline-experiment-control.readthedocs.io/en/latest/")
@staticmethod
def open_bec_widgets_docs():
webbrowser.open("https://bec.readthedocs.io/projects/bec-widgets/en/latest/")
@staticmethod
def open_bec_bug_report():
webbrowser.open("https://gitlab.psi.ch/groups/bec/-/issues/")

View File

@@ -7,7 +7,8 @@ import sys
from bec_ipython_client.main import BECIPythonClient
from qtpy.QtWidgets import QApplication
from tictactoe import TicTacToe
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)

View File

@@ -2,8 +2,9 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QPyDesignerCustomWidgetCollection
from tictactoe import TicTacToe
from tictactoeplugin import TicTacToePlugin
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
from bec_widgets.examples.plugin_example_pyside.tictactoeplugin import TicTacToePlugin
# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin

View File

@@ -4,10 +4,10 @@ import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from tictactoe import TicTacToe
from tictactoetaskmenu import TicTacToeTaskMenuFactory
import bec_widgets
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
from bec_widgets.examples.plugin_example_pyside.tictactoetaskmenu import TicTacToeTaskMenuFactory
DOM_XML = """
<ui language='c++'>

View File

@@ -1,11 +1,12 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtCore import Slot
from qtpy.QtDesigner import QExtensionFactory, QPyDesignerTaskMenuExtension
from qtpy.QtGui import QAction
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
from tictactoe import TicTacToe
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
class TicTacToeDialog(QDialog): # pragma: no cover

View File

@@ -6,7 +6,7 @@ from qtpy.QtCore import QObject, Qt, Signal, Slot
from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget
def SafeSlot(*slot_args, **slot_kwargs):
def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
"""Function with args, acting like a decorator, applying "error_managed" decorator + Qt Slot
to the passed function, to display errors instead of potentially raising an exception
@@ -34,10 +34,7 @@ class WarningPopupUtility(QObject):
Utility class to show warning popups in the application.
"""
def __init__(self, parent=None):
super().__init__(parent)
@Slot(str, str, str, QWidget)
@SafeSlot(str, str, str, QWidget)
def show_warning_message(self, title, message, detailed_text, widget):
msg = QMessageBox(widget)
msg.setIcon(QMessageBox.Warning)
@@ -60,7 +57,10 @@ class WarningPopupUtility(QObject):
self.show_warning_message(title, message, detailed_text, widget)
class ErrorPopupUtility(QObject):
_popup_utility_instance = None
class _ErrorPopupUtility(QObject):
"""
Utility class to manage error popups in the application to show error messages to the users.
This class is singleton and the error popup can be enabled or disabled globally or attach to widget methods with decorator @error_managed.
@@ -68,24 +68,14 @@ class ErrorPopupUtility(QObject):
error_occurred = Signal(str, str, QWidget)
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(ErrorPopupUtility, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, parent=None):
if not self._initialized:
super().__init__(parent=parent)
self.error_occurred.connect(self.show_error_message)
self.enable_error_popup = False
self._initialized = True
sys.excepthook = self.custom_exception_hook
super().__init__(parent=parent)
self.error_occurred.connect(self.show_error_message)
self.enable_error_popup = False
self._initialized = True
sys.excepthook = self.custom_exception_hook
@Slot(str, str, QWidget)
@SafeSlot(str, str, QWidget)
def show_error_message(self, title, message, widget):
detailed_text = self.format_traceback(message)
error_message = self.parse_error_message(detailed_text)
@@ -157,13 +147,12 @@ class ErrorPopupUtility(QObject):
"""
self.enable_error_popup = bool(state)
@classmethod
def reset_singleton(cls):
"""
Reset the singleton instance.
"""
cls._instance = None
cls._initialized = False
def ErrorPopupUtility():
global _popup_utility_instance
if not _popup_utility_instance:
_popup_utility_instance = _ErrorPopupUtility()
return _popup_utility_instance
class ExampleWidget(QWidget): # pragma: no cover

View File

@@ -1,6 +1,7 @@
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
class SettingWidget(QWidget):
"""
@@ -105,3 +106,14 @@ class SettingsDialog(QDialog):
Apply the changes made in the settings widget without closing the dialog.
"""
self.widget.accept_changes()
def cleanup(self):
"""
Cleanup the dialog.
"""
self.button_box.close()
self.button_box.deleteLater()
def closeEvent(self, event):
self.cleanup()
super().closeEvent(event)

View File

@@ -10,11 +10,11 @@ import yaml
from bec_lib.utils.import_utils import lazy_import_from
from pydantic import BaseModel, Field, field_validator
from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QApplication
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))

View File

@@ -2,11 +2,10 @@ import itertools
import re
from typing import Literal
import bec_qthemes
import numpy as np
import pyqtgraph as pg
import qdarkstyle
from pydantic_core import PydanticCustomError
from qdarkstyle import DarkPalette, LightPalette
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QApplication
@@ -14,7 +13,7 @@ CURRENT_THEME = "dark"
def get_theme_palette():
return DarkPalette if CURRENT_THEME == "dark" else LightPalette
return bec_qthemes.load_palette(CURRENT_THEME)
def apply_theme(theme: Literal["dark", "light"]):
@@ -30,7 +29,7 @@ def apply_theme(theme: Literal["dark", "light"]):
pg_widget.setBackground("k" if theme == "dark" else "w")
# now define stylesheet according to theme and apply it
style = qdarkstyle.load_stylesheet(palette=get_theme_palette())
style = bec_qthemes.load_stylesheet(theme)
app.setStyleSheet(style)

View File

@@ -1,12 +1,12 @@
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import Qt, Slot
from qtpy.QtWidgets import QHeaderView, QTableWidget, QTableWidgetItem, QWidget
from qtpy.QtWidgets import QHBoxLayout, QHeaderView, QTableWidget, QTableWidgetItem, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
class BECQueue(BECWidget, QTableWidget):
class BECQueue(BECWidget, QWidget):
"""
Widget to display the BEC queue.
"""
@@ -19,10 +19,14 @@ class BECQueue(BECWidget, QTableWidget):
gui_id: str = None,
):
super().__init__(client, config, gui_id)
QTableWidget.__init__(self, parent=parent)
self.setColumnCount(3)
self.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"])
header = self.horizontalHeader()
QWidget.__init__(self, parent=parent)
self.table = QTableWidget(self)
self.layout = QHBoxLayout(self)
self.layout.addWidget(self.table)
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"])
header = self.table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status())
self.reset_content()
@@ -38,8 +42,8 @@ class BECQueue(BECWidget, QTableWidget):
"""
# only show the primary queue for now
queue_info = content.get("queue", {}).get("primary", {}).get("info", [])
self.setRowCount(len(queue_info))
self.clearContents()
self.table.setRowCount(len(queue_info))
self.table.clearContents()
if not queue_info:
self.reset_content()
@@ -73,6 +77,8 @@ class BECQueue(BECWidget, QTableWidget):
Returns:
QTableWidgetItem: The formatted item.
"""
if not content or not isinstance(content, str):
content = ""
item = QTableWidgetItem(content)
item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
return item
@@ -88,16 +94,16 @@ class BECQueue(BECWidget, QTableWidget):
status (str): The status.
"""
self.setItem(index, 0, self.format_item(scan_number))
self.setItem(index, 1, self.format_item(scan_type))
self.setItem(index, 2, self.format_item(status))
self.table.setItem(index, 0, self.format_item(scan_number))
self.table.setItem(index, 1, self.format_item(scan_type))
self.table.setItem(index, 2, self.format_item(status))
def reset_content(self):
"""
Reset the content of the table.
"""
self.setRowCount(1)
self.table.setRowCount(1)
self.set_row(0, "", "", "")

View File

@@ -56,6 +56,11 @@ class BECServiceStatusMixin(QObject):
self.client._update_existing_services()
self.services_update.emit(self.client._services_info, self.client._services_metric)
def cleanup(self):
"""Cleanup the BECServiceStatusMixin."""
self._service_update_timer.stop()
self._service_update_timer.deleteLater()
class BECStatusBox(BECWidget, QWidget):
"""An autonomous widget to display the status of BEC services.
@@ -290,6 +295,11 @@ class BECStatusBox(BECWidget, QWidget):
if objects["item"] == item:
objects["widget"].show_popup()
def cleanup(self):
"""Cleanup the BECStatusBox widget."""
self.bec_service_status.cleanup()
return super().cleanup()
def main():
"""Main method to run the BECStatusBox widget."""

View File

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

View File

@@ -18,10 +18,11 @@ import pyte
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import QSize, QSocketNotifier, Qt
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QClipboard, QTextCursor
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
ansi_colors = {
"black": "#000000",
"red": "#CD0000",
@@ -289,7 +290,7 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
old["value"] = value
self.dataReady(self.backend.screen, reset_scroll=False)
@pyqtSlot(object)
@Slot(object)
def keyPressEvent(self, event):
"""
Redirect all keystrokes to the terminal process.

View File

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

View File

@@ -34,7 +34,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
):
super().__init__(client=client, config=config, gui_id=gui_id)
QComboBox.__init__(self, parent=parent)
self.setMinimumSize(125, 26)
self.populate_combobox()
if arg_name is not None:

View File

@@ -3,7 +3,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal, Optional
from pydantic import Field
from pyqtgraph.dockarea import Dock
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.")
@@ -25,6 +24,81 @@ 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:
fg = "#aaa"
bg = "#44a"
border = "#339"
else:
fg = "#fff"
bg = "#3f4042"
border = "#3f4042"
if self.orientation == "vertical":
self.vStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: 0px;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: %s;
border-width: 0px;
border-right: 2px solid %s;
padding-top: 3px;
padding-bottom: 3px;
font-size: %s;
}""" % (
bg,
fg,
r,
r,
border,
self.fontSize,
)
self.setStyleSheet(self.vStyle)
else:
self.hStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: %s;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
border-width: 0px;
border-bottom: 2px solid %s;
padding-left: 3px;
padding-right: 3px;
font-size: %s;
}""" % (
bg,
fg,
r,
r,
border,
self.fontSize,
)
self.setStyleSheet(self.hStyle)
class BECDock(BECWidget, Dock):
USER_ACCESS = [
"_config_dict",
@@ -51,6 +125,7 @@ class BECDock(BECWidget, Dock):
name: str | None = None,
client=None,
gui_id: str | None = None,
closable: bool = True,
**kwargs,
) -> None:
if config is None:
@@ -62,7 +137,9 @@ class BECDock(BECWidget, Dock):
config = DockConfig(**config)
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id)
Dock.__init__(self, name=name, **kwargs)
label = CustomDockLabel(text=name, closable=closable)
Dock.__init__(self, name=name, label=label, **kwargs)
# Dock.__init__(self, name=name, **kwargs)
self.parent_dock_area = parent_dock_area
@@ -76,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):
"""
@@ -207,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):
"""
@@ -242,6 +320,8 @@ class BECDock(BECWidget, Dock):
if hasattr(widget, "cleanup"):
widget.cleanup()
self.widgets.clear()
self.label.close()
self.label.deleteLater()
super().cleanup()
def close(self):

View File

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

View File

@@ -164,7 +164,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
def __getitem__(self, key: tuple | str):
if isinstance(key, tuple) and len(key) == 2:
return self.axes(*key)
elif isinstance(key, str):
if isinstance(key, str):
widget = self._widgets.get(key)
if widget is None:
raise KeyError(f"No widget with ID {key}")
@@ -185,7 +185,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
self.change_theme(self.config.theme)
# widget_config has to be reset for not have each widget config twice when added to the figure
widget_configs = [config for config in self.config.widgets.values()]
widget_configs = list(self.config.widgets.values())
self.config.widgets = {}
for widget_config in widget_configs:
getattr(self, self.widget_method_map[widget_config.widget_class])(
@@ -233,8 +233,8 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
"""Export the plot widget."""
try:
plot_item = self.widget_list[0]
except:
raise ValueError("No plot widget available to export.")
except Exception as exc:
raise ValueError("No plot widget available to export.") from exc
scene = plot_item.scene()
scene.contextMenuItem = plot_item
@@ -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.")
else:
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
@@ -721,7 +717,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
# Populate the new grid with widgets' IDs
current_idx = 0
for widget_id, widget in self._widgets.items():
for widget_id in self._widgets:
row = current_idx // len(new_grid[0])
col = current_idx % len(new_grid[0])
new_grid[row][col] = widget_id
@@ -745,3 +741,12 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
self.config = FigureConfig(
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
)
def cleanup_pyqtgraph_all_widgets(self):
"""Clean up the pyqtgraph widget."""
for widget in self.widget_list:
widget.cleanup_pyqtgraph()
def cleanup(self):
"""Close the figure widget."""
self.cleanup_pyqtgraph_all_widgets()

View File

@@ -1,8 +1,8 @@
import os
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QVBoxLayout
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.qt_utils.settings_dialog import SettingWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.widget_io import WidgetIO

View File

@@ -7,9 +7,9 @@ import numpy as np
from bec_lib.endpoints import MessageEndpoints
from pydantic import BaseModel, Field, ValidationError
from qtpy.QtCore import QThread
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import EntryValidator
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
from bec_widgets.widgets.figure.plots.image.image_processor import (
@@ -247,7 +247,7 @@ class BECImageShow(BECPlotBase):
Returns:
BECImageItem: The image item.
"""
image_source = "device_monitor"
image_source = "device_monitor_2d"
image_exits = self._check_image_id(monitor, self._images)
if image_exits:
@@ -287,7 +287,7 @@ class BECImageShow(BECPlotBase):
**kwargs,
):
image_source = "custom"
# image_source = "device_monitor"
# image_source = "device_monitor_2d"
image_exits = self._check_image_id(name, self._images)
if image_exits:
@@ -500,21 +500,22 @@ class BECImageShow(BECPlotBase):
self.update_image(device, data)
self.update_vrange(device, self.processor.config.stats)
@pyqtSlot(dict)
def on_image_update(self, msg: dict):
@Slot(dict, dict)
def on_image_update(self, msg: dict, metadata: dict):
"""
Update the image of the device monitor from bec.
Args:
msg(dict): The message from bec.
metadata(dict): The metadata of the message.
"""
data = msg["data"]
device = msg["device"]
image = self._images["device_monitor"][device]
image = self._images["device_monitor_2d"][device]
image.raw_data = data
self.process_image(device, image, data)
@pyqtSlot(str, np.ndarray)
@Slot(str, np.ndarray)
def update_image(self, device: str, data: np.ndarray):
"""
Update the image of the device monitor.
@@ -523,10 +524,10 @@ class BECImageShow(BECPlotBase):
device(str): The name of the device.
data(np.ndarray): The data to be updated.
"""
image_to_update = self._images["device_monitor"][device]
image_to_update = self._images["device_monitor_2d"][device]
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
@pyqtSlot(str, ImageStats)
@Slot(str, ImageStats)
def update_vrange(self, device: str, stats: ImageStats):
"""
Update the scaling of the image.
@@ -534,7 +535,7 @@ class BECImageShow(BECPlotBase):
Args:
stats(ImageStats): The statistics of the image.
"""
image_to_update = self._images["device_monitor"][device]
image_to_update = self._images["device_monitor_2d"][device]
if image_to_update.config.autorange:
image_to_update.auto_update_vrange(stats)
@@ -547,7 +548,7 @@ class BECImageShow(BECPlotBase):
data = image.raw_data
self.process_image(image_id, image, data)
def _connect_device_monitor(self, monitor: str):
def _connect_device_monitor_2d(self, monitor: str):
"""
Connect to the device monitor.
@@ -561,13 +562,13 @@ class BECImageShow(BECPlotBase):
previous_monitor = None
if previous_monitor and image_item.connected is True:
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(previous_monitor)
self.on_image_update, MessageEndpoints.device_monitor_2d(previous_monitor)
)
image_item.connected = False
if monitor and image_item.connected is False:
self.entry_validator.validate_monitor(monitor)
self.bec_dispatcher.connect_slot(
self.on_image_update, MessageEndpoints.device_monitor(monitor)
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
)
image_item.set_monitor(monitor)
image_item.connected = True
@@ -581,8 +582,8 @@ class BECImageShow(BECPlotBase):
if self.single_image is True and len(self.images) > 0:
self.remove_image(0)
self._images[source][name] = image
if source == "device_monitor":
self._connect_device_monitor(config.monitor)
if source == "device_monitor_2d":
self._connect_device_monitor_2d(config.monitor)
self.config.images[name] = config
if data is not None:
image.setImage(data)
@@ -668,15 +669,29 @@ class BECImageShow(BECPlotBase):
image = self.find_image_by_monitor(image_id)
if image:
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(image.config.monitor)
self.on_image_update, MessageEndpoints.device_monitor_2d(image.config.monitor)
)
def cleanup(self):
"""
Clean up the widget.
"""
for monitor in self._images["device_monitor"]:
for monitor in self._images["device_monitor_2d"]:
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(monitor)
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
)
self.images.clear()
def cleanup_pyqtgraph(self):
"""Cleanup pyqtgraph items."""
super().cleanup_pyqtgraph()
item = self.plot_item
if not item.items:
return
cbar = item.items[0].color_bar
cbar.vb.menu.close()
cbar.vb.menu.deleteLater()
cbar.gradient.menu.close()
cbar.gradient.menu.deleteLater()
cbar.gradient.colorDialog.close()
cbar.gradient.colorDialog.deleteLater()

View File

@@ -10,9 +10,9 @@ from pydantic import Field, ValidationError, field_validator
from pydantic_core import PydanticCustomError
from qtpy import QtCore, QtGui
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import Colors, EntryValidator
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
from bec_widgets.widgets.figure.plots.waveform.waveform import Signal, SignalData
@@ -444,7 +444,7 @@ class BECMotorMap(BECPlotBase):
return None
@Slot()
def _update_plot(self):
def _update_plot(self, _=None):
"""Update the motor map plot."""
# If the number of points exceeds max_points, delete the oldest points
if len(self.database_buffer["x"]) > self.config.max_points:
@@ -493,13 +493,14 @@ class BECMotorMap(BECPlotBase):
f"Motor position: ({round(float(current_x),precision)}, {round(float(current_y),precision)})"
)
@Slot(dict)
def on_device_readback(self, msg: dict) -> None:
@Slot(dict, dict)
def on_device_readback(self, msg: dict, metadata: dict) -> None:
"""
Update the motor map plot with the new motor position.
Args:
msg(dict): Message from the device readback.
metadata(dict): Metadata of the message.
"""
if self.motor_x is None or self.motor_y is None:
return

View File

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

View File

@@ -11,9 +11,9 @@ from bec_lib.endpoints import MessageEndpoints
from pydantic import Field, ValidationError, field_validator
from pyqtgraph.exporters import MatplotlibExporter
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import Colors, EntryValidator
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
@@ -391,7 +391,7 @@ class BECWaveform(BECPlotBase):
self.async_signal_update.emit()
self.scan_signal_update.emit()
@pyqtSlot()
@Slot()
def auto_range(self):
self.plot_item.autoRange()
@@ -408,7 +408,7 @@ class BECWaveform(BECPlotBase):
"""
self.plot_item.enableAutoRange(axis, enabled)
@pyqtSlot()
@Slot()
def auto_range(self):
self.plot_item.autoRange()
@@ -642,7 +642,7 @@ class BECWaveform(BECPlotBase):
self.refresh_dap()
return curve
@pyqtSlot()
@Slot()
def get_dap_params(self) -> dict:
"""
Get the DAP parameters of all DAP curves.
@@ -655,7 +655,7 @@ class BECWaveform(BECPlotBase):
params[curve_id] = curve.dap_params
return params
@pyqtSlot()
@Slot()
def get_dap_summary(self) -> dict:
"""
Get the DAP summary of all DAP curves.
@@ -921,7 +921,7 @@ class BECWaveform(BECPlotBase):
else:
raise IndexError(f"Curve order {N} out of range.")
@pyqtSlot(dict)
@Slot(dict)
def on_scan_status(self, msg):
"""
Handle the scan status message.
@@ -945,7 +945,7 @@ class BECWaveform(BECPlotBase):
for curve_id, curve in self._curves_data["async"].items():
self.setup_async(curve.config.signals.y.name)
@pyqtSlot(dict, dict)
@Slot(dict, dict)
def on_scan_segment(self, msg: dict, metadata: dict):
"""
Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
@@ -1004,7 +1004,7 @@ class BECWaveform(BECPlotBase):
self.update_dap, MessageEndpoints.dap_response(f"{new_scan_id}-{self.gui_id}")
)
@pyqtSlot(str)
@Slot(str)
def setup_async(self, device: str):
self.bec_dispatcher.disconnect_slot(
self.on_async_readback, MessageEndpoints.device_async_readback(self.old_scan_id, device)
@@ -1020,8 +1020,8 @@ class BECWaveform(BECPlotBase):
from_start=True,
)
@pyqtSlot()
def refresh_dap(self):
@Slot()
def refresh_dap(self, _=None):
"""
Refresh the DAP curves with the latest data from the DAP model MessageEndpoints.dap_response().
"""
@@ -1069,7 +1069,7 @@ class BECWaveform(BECPlotBase):
)
self.client.connector.set_and_publish(MessageEndpoints.dap_request(), msg)
@pyqtSlot(dict, dict)
@Slot(dict, dict)
def update_dap(self, msg, metadata):
self.msg = msg
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
@@ -1089,7 +1089,7 @@ class BECWaveform(BECPlotBase):
self.dap_summary_update.emit(curve.dap_summary)
break
@pyqtSlot(dict, dict)
@Slot(dict, dict)
def on_async_readback(self, msg, metadata):
"""
Get async data readback.
@@ -1127,7 +1127,7 @@ class BECWaveform(BECPlotBase):
else:
curve.setData(data_plot)
@pyqtSlot()
@Slot()
def replot_async_curve(self):
try:
data = self.scan_item.async_data
@@ -1152,8 +1152,8 @@ class BECWaveform(BECPlotBase):
else:
curve.setData(data_x, data_y)
@pyqtSlot()
def _update_scan_curves(self):
@Slot()
def _update_scan_curves(self, _=None):
"""
Update the scan curves with the data from the scan segment.
"""

View File

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

View File

@@ -1,8 +1,8 @@
import os
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QVBoxLayout
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.qt_utils.settings_dialog import SettingWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.widget_io import WidgetIO
@@ -45,3 +45,12 @@ class MotorMapSettings(SettingWidget):
self.target_widget.set_scatter_size(scatter_size)
self.target_widget.set_background_value(background_intensity)
self.target_widget.set_color(color)
def cleanup(self):
self.ui.color.cleanup()
self.ui.color.close()
self.ui.color.deleteLater()
def closeEvent(self, event):
self.cleanup()
super().closeEvent(event)

View File

@@ -1,7 +1,11 @@
""" 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
@@ -11,12 +15,23 @@ from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
logger = bec_logger.logger
class DeviceBox(BECWidget, QWidget):
class PositionerBox(BECWidget, QWidget):
"""Simple Widget to control a positioner in box form"""
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 +44,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, "positioner_box.ui"))
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
@@ -57,28 +73,73 @@ class DeviceBox(BECWidget, QWidget):
self.ui.spinner_widget.start()
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 +159,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 +201,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 +219,7 @@ class DeviceBox(BECWidget, QWidget):
@Slot()
def on_stop(self):
"""Stop call"""
request_id = str(uuid.uuid4())
params = {
"device": self.device,
@@ -165,18 +238,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)
@@ -191,7 +268,7 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
apply_theme("light")
widget = DeviceBox(device="samx")
widget = PositionerBox(device="bpm4i")
widget.show()
sys.exit(app.exec_())

View File

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

View File

@@ -29,17 +29,24 @@
<item>
<widget class="QGroupBox" name="device_box">
<property name="title">
<string>Device Name</string>
<string>No positioner selected</string>
</property>
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0">
<property name="topMargin">
<number>0</number>
</property>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="step_size"/>
<widget class="QDoubleSpinBox" name="step_size">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="tweak_right">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
@@ -67,10 +74,17 @@
</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>
</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 +113,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>

View File

@@ -1,44 +1,39 @@
# 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__)
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):
return DOM_XML
def group(self):
return "Device Control"
return ""
def icon(self):
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "device_box.png")
return QIcon(icon_path)
return QIcon()
def includeFile(self):
return "device_box"
return "positioner_box"
def initialize(self, form_editor):
self._form_editor = form_editor
@@ -50,10 +45,10 @@ class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return self._form_editor is not None
def name(self):
return "DeviceBox"
return "PositionerBox"
def toolTip(self):
return "A widget for controlling a single positioner. "
return "Simple Widget to control a positioner in box form"
def whatsThis(self):
return self.toolTip()

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ class SpinnerWidget(QWidget):
color_palette = get_theme_palette()
color = QColor(color_palette.COLOR_ACCENT_4)
color = QColor(color_palette.accent().color())
rect.adjust(line_width, line_width, -line_width, -line_width)
@@ -75,6 +75,10 @@ class SpinnerWidget(QWidget):
painter.drawArc(adjusted_rect, self.angle * 16, int(angle_span))
painter.end()
def closeEvent(self, event):
self.timer.stop()
super().closeEvent(event)
if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)

View File

@@ -1,6 +1,6 @@
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QPushButton
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils.bec_widget import BECWidget

View File

@@ -1,8 +1,8 @@
import os
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QDialog, QTreeWidgetItem, QVBoxLayout
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import UILoader

View File

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

View File

@@ -6,7 +6,6 @@ pydata-sphinx-theme
sphinx-copybutton
myst-parser
sphinx-design
PyQt6
PyQt6-WebEngine
PySide6
bec-widgets
tomli

View File

@@ -11,4 +11,5 @@ hidden: true
installation/
quick_start/
auto_updates/
video_tutorials/
```

View File

@@ -0,0 +1,17 @@
(user.video_tutorials)=
# Video Tutorials
This section includes video tutorials that demonstrate various use cases of `bec-widgets`, including video tutorials,
presentations, and conference talks.
## BSEG Meeting 24th July 2024
This video is a presentation of the BEC Widgets project at the BSEG meeting on the 24th July 2024. The presentation
covers the basic interactions, including visualization of live data acquisition and how to steer experiments using
user-friendly tools. Learn how BEC Widgets can enhance your experimental control projects with its intuitive interface
and powerful features.
Used version of BEC Widgets: 0.91.0
<iframe width="560" height="315" src="https://www.youtube.com/embed/qZ8fWXRAdHE" frameborder="0" allowfullscreen></iframe>

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "0.91.0"
version = "0.93.4"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [
@@ -19,7 +19,7 @@ dependencies = [
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
"pydantic~=2.0",
"pyqtgraph~=0.13",
"qdarkstyle>=3.2.2",
"bec_qthemes~=0.0",
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
"qtpy~=2.4",
"pyte", # needed for vt100 console

View File

@@ -98,7 +98,7 @@ def test_rpc_add_dock_with_figure_e2e(bec_client_lib, rpc_server_dock):
assert plt_data["bpm4i-bpm4i"]["y"] == plt_last_scan_data["bpm4i"]["bpm4i"].val
# image
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor_2d("eiger"))[
"data"
].data
time.sleep(0.5)

View File

@@ -120,7 +120,7 @@ def test_rpc_image(rpc_server_figure, bec_client_lib):
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
status.wait()
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor_2d("eiger"))[
"data"
].data
last_image_plot = im.images[0].get_data()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,9 +1,25 @@
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
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
@pytest.fixture(autouse=True)
def qapplication(qtbot): # pylint: disable=unused-argument
yield
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)
def rpc_register():
yield RPCRegister()
@@ -11,7 +27,7 @@ def rpc_register():
@pytest.fixture(autouse=True)
def bec_dispatcher(threads_check):
def bec_dispatcher(threads_check): # pylint: disable=unused-argument
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
yield bec_dispatcher
bec_dispatcher.disconnect_all()
@@ -19,3 +35,30 @@ def bec_dispatcher(threads_check):
bec_dispatcher.client.shutdown()
# reinitialize singleton for next test
bec_dispatcher_module.BECDispatcher.reset_singleton()
@pytest.fixture(autouse=True)
def clean_singleton():
error_popups._popup_utility_instance = None
def create_widget(qtbot, widget, *args, **kwargs):
"""
Create a widget and add it to the qtbot for testing. This is a helper function that
should be used in all tests that require a widget to be created.
DO NOT CREATE WIDGETS DIRECTLY IN A FIXTURE!
Args:
qtbot (fixture): pytest-qt fixture
widget (QWidget): widget class to be created
*args: positional arguments for the widget
**kwargs: keyword arguments for the widget
Returns:
QWidget: the created widget
"""
widget = widget(*args, **kwargs)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
return widget

View File

@@ -2,9 +2,9 @@
import time
import pytest
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QApplication
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import BECConnector, ConnectionConfig
from .client_mocks import mocked_client

View File

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

View File

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

View File

@@ -6,8 +6,10 @@ import pytest
from bec_lib import messages
from qtpy.QtGui import QFontInfo
from bec_widgets.widgets.figure import BECFigure
from .client_mocks import mocked_client
from .test_bec_figure import bec_figure
from .conftest import create_widget
@pytest.fixture
@@ -15,23 +17,21 @@ 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.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
bec_image_show.on_image_update(msg)
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
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.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
bec_image_show.on_image_update(msg)
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
assert np.array_equal(img.get_data(), data)
vmin = max(np.mean(data) - 2 * np.std(data), 0)
@@ -39,7 +39,7 @@ def test_autorange_on_image_update(bec_image_show):
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
# Test general update with autorange True, mode "max"
bec_image_show.set_autorange_mode("max")
bec_image_show.on_image_update(msg)
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
vmin = np.min(data)
vmax = np.max(data)
@@ -47,18 +47,16 @@ def test_autorange_on_image_update(bec_image_show):
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
# Change the input data, and switch to autorange False, colormap levels should stay untouched
data *= 100
msg = messages.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
bec_image_show.set_autorange(False)
bec_image_show.on_image_update(msg)
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
assert np.array_equal(img.get_data(), data)
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-3, 1e-3)).all()
# Reactivate autorange, should now scale the new data
bec_image_show.set_autorange(True)
bec_image_show.set_autorange_mode("mean")
bec_image_show.on_image_update(msg)
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
vmin = max(np.mean(data) - 2 * np.std(data), 0)
vmax = np.mean(data) + 2 * np.std(data)

View File

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

View File

@@ -1,14 +1,17 @@
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())
@@ -17,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"
@@ -31,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]}
@@ -40,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)
@@ -56,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
@@ -72,7 +79,8 @@ def test_motor_movement_updates_position_and_database(bec_figure):
# Simulate motor movement for 'samx' only
new_position_samx = 4.0
mm.on_device_readback({"signals": {"samx": {"value": new_position_samx}}})
msg = DeviceMessage(signals={"samx": {"value": new_position_samx}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
init_positions["samx"].append(new_position_samx)
init_positions["samy"].append(init_positions["samy"][-1])
@@ -83,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
@@ -96,7 +105,8 @@ def test_scatter_plot_rendering(bec_figure):
# Simulate motor movement for 'samx' only
new_position_samx = 4.0
mm.on_device_readback({"signals": {"samx": {"value": new_position_samx}}})
msg = DeviceMessage(signals={"samx": {"value": new_position_samx}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
mm._update_plot()
# Get the scatter plot item
@@ -112,12 +122,15 @@ 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
mm.on_device_readback({"signals": {"samx": {"value": 5}}})
mm.on_device_readback({"signals": {"samy": {"value": 9}}})
msg = DeviceMessage(signals={"samx": {"value": 5}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
msg = DeviceMessage(signals={"samy": {"value": 9}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
mm._update_plot()
scatter_plot_item = mm.plot_components["scatter"]
@@ -128,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
@@ -141,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",
@@ -195,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
@@ -208,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
@@ -216,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)
@@ -226,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
@@ -234,10 +253,14 @@ def test_motor_map_get_data_max_points(bec_figure, qtbot):
"samx": [motor_map_dev["samx"].read()["samx"]["value"]],
"samy": [motor_map_dev["samy"].read()["samy"]["value"]],
}
mm.on_device_readback({"signals": {"samx": {"value": 5.0}}})
mm.on_device_readback({"signals": {"samy": {"value": 9.0}}})
mm.on_device_readback({"signals": {"samx": {"value": 6.0}}})
mm.on_device_readback({"signals": {"samy": {"value": 7.0}}})
msg = DeviceMessage(signals={"samx": {"value": 5.0}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
msg = DeviceMessage(signals={"samy": {"value": 9.0}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
msg = DeviceMessage(signals={"samx": {"value": 6.0}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
msg = DeviceMessage(signals={"samy": {"value": 7.0}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
expected_x = [init_positions["samx"][-1], 5.0, 5.0, 6.0, 6.0]
expected_y = [init_positions["samy"][-1], init_positions["samy"][-1], 9.0, 9.0, 7.0]

View File

@@ -97,15 +97,15 @@ def bec_queue(qtbot, mocked_client):
def test_bec_queue(bec_queue, bec_queue_msg_full):
bec_queue.update_queue(bec_queue_msg_full.content, {})
assert bec_queue.rowCount() == 1
assert bec_queue.item(0, 0).text() == "1289"
assert bec_queue.item(0, 1).text() == "line_scan"
assert bec_queue.item(0, 2).text() == "COMPLETED"
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
def test_bec_queue_empty(bec_queue):
bec_queue.update_queue({}, {})
assert bec_queue.rowCount() == 1
assert bec_queue.item(0, 0).text() == ""
assert bec_queue.item(0, 1).text() == ""
assert bec_queue.item(0, 2).text() == ""
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == ""
assert bec_queue.table.item(0, 1).text() == ""
assert bec_queue.table.item(0, 2).text() == ""

View File

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

View File

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

View File

@@ -8,12 +8,16 @@ from .client_mocks import mocked_client
# DeviceInputBase is meant to be mixed in a QWidget
class DeviceInputWidget(DeviceInputBase, QWidget):
pass
def __init__(self, parent=None, client=None, config=None, gui_id=None):
super().__init__(client=client, config=config, gui_id=gui_id)
QWidget.__init__(self, parent=parent)
@pytest.fixture
def device_input_base(mocked_client):
def device_input_base(qtbot, mocked_client):
widget = DeviceInputWidget(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,104 @@
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 .client_mocks import mocked_client
@pytest.fixture
def positioner_box(qtbot, mocked_client):
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):
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):
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):
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):
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):
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):
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):
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

View File

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

View File

@@ -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):
@@ -46,23 +45,23 @@ def test_setting_widget_display_current_settings(setting_widget):
# SettingsDialog tests
###################################
@pytest.fixture
def settings_dialog(qtbot):
def settings_dialog(qtbot, setting_widget):
parent_widget = QWidget()
settings_widget = SettingWidget()
settings_widget.set_target_widget = MagicMock()
settings_widget.display_current_settings = MagicMock()
settings_widget.accept_changes = MagicMock()
setting_widget.set_target_widget = MagicMock()
setting_widget.display_current_settings = MagicMock()
setting_widget.accept_changes = MagicMock()
dialog = SettingsDialog(
parent=parent_widget,
settings_widget=settings_widget,
settings_widget=setting_widget,
window_title="Test Settings",
config={"setting1": "value1", "setting2": "value2"},
)
qtbot.addWidget(dialog)
qtbot.waitExposed(dialog)
yield dialog, parent_widget, settings_widget
dialog.close()
yield dialog, parent_widget, setting_widget
parent_widget.close()
parent_widget.deleteLater()
def test_settings_dialog_initialization(settings_dialog):

View File

@@ -12,6 +12,7 @@ def spinner_widget(qtbot):
qtbot.addWidget(spinner)
qtbot.waitExposed(spinner)
yield spinner
spinner.close()
def test_spinner_widget_paint_event(spinner_widget, qtbot):

View File

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

View File

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

View File

@@ -14,6 +14,8 @@ 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.waitExposed(widget)
yield widget

View File

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

View File

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

View File

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