Compare commits
90 Commits
v0.56.0
...
feat/bash-
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e52550f67 | |||
| 74182bb142 | |||
| 2b5068903a | |||
|
|
718950cf0d | ||
| 17a0068757 | |||
| abc6caa2d0 | |||
|
|
99fb82561b | ||
| 61ba08d0b8 | |||
| 40b5688158 | |||
|
|
0a4e253cbd | ||
| 6428e38ab9 | |||
| fc4f4f81ad | |||
| f6629852eb | |||
| 3adf6cfd58 | |||
| b15816ca9f | |||
| 6b1d5827d6 | |||
| f0391f59c9 | |||
| 006a0894b8 | |||
| 9c5a471234 | |||
| 1c7f4912ce | |||
| df1be10057 | |||
| 954c576131 | |||
| 867720a897 | |||
| 2b40602bdc | |||
| 11173b9c0a | |||
|
|
52d46e77db | ||
| e7838b0f2f | |||
|
|
2ae3810cf6 | ||
| 178fe4d2da | |||
| 2d79ef8fe5 | |||
| d56c5493cd | |||
| cf6e5a40fc | |||
| 64abd67b9b | |||
|
|
c19e856800 | ||
| 02a26086c4 | |||
|
|
35f880bc2f | ||
| c0ddeceeea | |||
| 67fd5e8581 | |||
| bf699ec1fb | |||
| 3094632134 | |||
| 6985ff0fce | |||
| 33f7be42c5 | |||
|
|
36fac70361 | ||
| ca5e8d2fbb | |||
| 828067f486 | |||
|
|
e5f4c0b952 | ||
| edb1775967 | |||
|
|
d0d6908a74 | ||
| c037b87675 | |||
| 52bc322b2b | |||
| 8479caf53a | |||
| 82e2c898d2 | |||
|
|
4852076e4a | ||
| 15cbc21e5b | |||
| ffae5ee54e | |||
|
|
10d77f20d1 | ||
| 4be0d14b74 | |||
| e883dbad81 | |||
| a2abad344f | |||
| d44b1cf8b1 | |||
| c5b6499e41 | |||
| a951ebf1be | |||
| 32da803df9 | |||
| 07d60cf735 | |||
|
|
fceb851c32 | ||
| e1af5ca60f | |||
| 7fb31fc4d7 | |||
| 5c6ba65469 | |||
| cd9fc46ff8 | |||
| 2a88e17b23 | |||
| 69f4371007 | |||
|
|
af2c816d35 | ||
| f51b25f0af | |||
| c3f4845b4f | |||
| 8ae323f5c3 | |||
|
|
a3805a765b | ||
| 8c03034acf | |||
| 4160f3d6d7 | |||
|
|
4742e1ff6a | ||
| 4af1abe4e1 | |||
| 131f49da8e | |||
|
|
f9bf496bd3 | ||
| 9648e3ea96 | |||
| 4be756a867 | |||
| 46face0ee5 | |||
| 6f3b1ea985 | |||
| 3c9181d93d | |||
|
|
a05f24785e | ||
| 9d615c915c | |||
| d2539918b2 |
175
.gitlab-ci.yml
@@ -31,6 +31,20 @@ stages:
|
||||
- End2End
|
||||
- Deploy
|
||||
|
||||
.install-qt-webengine-deps: &install-qt-webengine-deps
|
||||
- apt-get -y install libnss3 libxdamage1 libasound2 libatomic1 libxcursor1
|
||||
- export QTWEBENGINE_DISABLE_SANDBOX=1
|
||||
|
||||
.clone-repos: &clone-repos
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
|
||||
.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
|
||||
- *install-qt-webengine-deps
|
||||
|
||||
before_script:
|
||||
- if [[ "$CI_PROJECT_PATH" != "bec/bec_widgets" ]]; then
|
||||
echo -e "\033[35;1m Using branch $CHILD_PIPELINE_BRANCH of BEC Widgets \033[0;m";
|
||||
@@ -75,18 +89,21 @@ pylint-check:
|
||||
- apt-get update
|
||||
- apt-get install -y bc
|
||||
script:
|
||||
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
|
||||
# Identify changed Python files
|
||||
- if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
|
||||
TARGET_BRANCH_COMMIT_SHA=$(git rev-parse $CI_MERGE_REQUEST_TARGET_BRANCH_NAME);
|
||||
CHANGED_FILES=$(git diff --name-only $SOURCE_BRANCH_COMMIT_SHA $TARGET_BRANCH_COMMIT_SHA | grep '\.py$' || true);
|
||||
TARGET_BRANCH_COMMIT_SHA=$(git rev-parse origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME);
|
||||
CHANGED_FILES=$(git diff --name-only $TARGET_BRANCH_COMMIT_SHA HEAD | grep '\.py$' || true);
|
||||
else
|
||||
CHANGED_FILES=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | grep '\.py$' || true);
|
||||
fi
|
||||
- if [ -z "$CHANGED_FILES" ]; then echo "No Python files changed."; exit 0; fi
|
||||
|
||||
- echo "Changed Python files:"
|
||||
- $CHANGED_FILES
|
||||
# Run pylint only on changed files
|
||||
- mkdir ./pylint
|
||||
- pylint $CHANGED_FILES --output-format=text . | tee ./pylint/pylint_changed_files.log || pylint-exit $?
|
||||
- pylint $CHANGED_FILES --output-format=text | tee ./pylint/pylint_changed_files.log || pylint-exit $?
|
||||
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint_changed_files.log)
|
||||
- echo "Pylint score is $PYLINT_SCORE"
|
||||
|
||||
@@ -105,12 +122,10 @@ tests:
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e ./bec/bec_ipython_client
|
||||
- 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
|
||||
@@ -123,123 +138,31 @@ tests:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
tests-3.10-pyside6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
test-matrix:
|
||||
parallel:
|
||||
matrix:
|
||||
- PYTHON_VERSION:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
QT_PCKG:
|
||||
- "pyside6"
|
||||
- "pyqt5"
|
||||
- "pyqt6"
|
||||
|
||||
tests-3.12-pyside6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
|
||||
needs: []
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
PYTHON_VERSION: ""
|
||||
QT_PCKG: ""
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:$PYTHON_VERSION
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.10-pyqt5:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt5]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.11-pyqt5:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt5]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.12-pyqt5:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt5]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.10-pyqt6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.11-pyqt6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.12-pyqt6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- pip install -e ./bec/bec_ipython_client
|
||||
- pip install -e .[dev,$QT_PCKG]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
@@ -251,8 +174,8 @@ end-2-end-conda:
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- conda config --prepend channels conda-forge
|
||||
- conda config --set channel_priority strict
|
||||
- conda config --set always_yes yes --set changeps1 no
|
||||
@@ -261,10 +184,6 @@ end-2-end-conda:
|
||||
- source ~/.bashrc
|
||||
- conda activate test-environment
|
||||
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
|
||||
- cd ./bec
|
||||
- source ./bin/install_bec_dev.sh -t
|
||||
|
||||
@@ -273,7 +192,7 @@ end-2-end-conda:
|
||||
- cd ../
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- cd ./tests/end-2-end
|
||||
- pytest --start-servers --flush-redis --random-order
|
||||
- pytest -v --start-servers --flush-redis --random-order
|
||||
|
||||
artifacts:
|
||||
when: on_failure
|
||||
|
||||
268
CHANGELOG.md
@@ -2,182 +2,164 @@
|
||||
|
||||
|
||||
|
||||
## v0.56.0 (2024-05-29)
|
||||
|
||||
### Build
|
||||
|
||||
* build: added pyside6 as dependency ([`db301b1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/db301b1be27bba76c8bb21fbff93cb4902b592a5))
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: added tests for pyside6, pyqt6 and pyqt5, default test and e2e is python 3.11 and pyqt6 ([`855be35`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/855be3551a1372bcbebba6f8930903f99202bbca))
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(examples): example apps section deleted ([`ad208a5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ad208a5ef8495c45a8b83a4850ba9a1041b42717))
|
||||
## v0.62.0 (2024-06-12)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(utils/ui_loader): universal ui loader for pyside/pyqt ([`0fea8d6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0fea8d606574fa99dda3b117da5d5209c251f694))
|
||||
* feat: implement non-polling, interruptible waiting of gui instruction response with timeout ([`abc6caa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3))
|
||||
|
||||
### Fix
|
||||
### Unknown
|
||||
|
||||
* fix(examples): outdated examples removed (mca_plot.py, stream_plot.py, motor_example.py) ([`ddc9510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ddc9510c2ba8dadf291809eeb5b135a105259492))
|
||||
|
||||
* fix: compatibility adjustment to .ui loading and tests for PySide6 ([`07b99d9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/07b99d91a57a645cddd76294f48d78773e4c9ea5))
|
||||
* doc: add documentation about creating custom GUI applications embedding BEC Widgets ([`17a0068`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/17a00687579f5efab1990cd83862ec0e78198633))
|
||||
|
||||
|
||||
## v0.55.0 (2024-05-24)
|
||||
## v0.61.0 (2024-06-12)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(widgets/progressbar): SpiralProgressBar added with rpc interface ([`76bd0d3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/76bd0d339ac9ae9e8a3baa0d0d4e951ec1d09670))
|
||||
|
||||
|
||||
## v0.54.0 (2024-05-24)
|
||||
|
||||
### Build
|
||||
|
||||
* build: added pyqt6 as sphinx build dependency ([`a47a8ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a47a8ec413934cf7fce8d5b7a5913371d4b3b4a5))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(figure): changes to support direct plot functionality ([`fc4d0f3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc4d0f3bb2a7c2fca9c326d86eb68b305bcd548b))
|
||||
* feat(widgets/stop_button): General stop button added ([`61ba08d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61ba08d0b8df9f48f5c54c7c2b4e6d395206e7e6))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(reconstruction): repository structure is changed to separate assets needed for each widget ([`3455c60`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3455c602361d3b5cc3ff9190f9d2870474becf8a))
|
||||
|
||||
* refactor(clean-up): 1st generation widgets are removed ([`edc25fb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/edc25fbf9d5a0321e5f0a80b492b6337df807849))
|
||||
* refactor: improve labe of auto_update script ([`40b5688`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/40b568815893cd41af3531bb2e647ca1e2e315f4))
|
||||
|
||||
|
||||
## v0.53.3 (2024-05-16)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: removed apparently unnecessary sleep while waiting for an rpc response ([`7d64cac`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7d64cac6610b39d3553ff650354f78ead8ee6b55))
|
||||
|
||||
|
||||
## v0.53.2 (2024-05-15)
|
||||
## v0.60.0 (2024-06-08)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: added echo to highlight the current branch ([`0490e80`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0490e80c48563e4fb486bce903b3ce1f08863e83))
|
||||
* ci: added git fetch for target branch ([`fc4f4f8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc4f4f81ad1be99cf5112f2188a46c5bed2679ee))
|
||||
|
||||
### Fix
|
||||
* ci: fixed pylint-check ([`6b1d582`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b1d5827d6599f06a3acd316060a8d25f0686d54))
|
||||
|
||||
* fix: check device class without importing to speed up initial import time ([`9f8fbdd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9f8fbdd5fc13cf2be10eacb41e10cf742864cd92))
|
||||
|
||||
* fix: speed up initial import times using lazy import (from bec_lib) ([`d1e6cd3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d1e6cd388c6c9f345f52d6096d8a75a1fa7e6934))
|
||||
|
||||
* fix: adapt to bec_lib changes (no more submodules in `__init__.py`) ([`5d09a13`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d09a13d8820a8bdb900733c97593b723a2fce1d))
|
||||
|
||||
|
||||
## v0.53.1 (2024-05-09)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: fixed rtd pages url ([`8ff3610`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ff36105d1e637c429915b4bfc2852d54a3c6f19))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: docs config ([`0f6a5e5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0f6a5e5fa9530969c98a9266c9ca7b89a378ff70))
|
||||
|
||||
|
||||
## v0.53.0 (2024-05-09)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: use formatter config of toml file ([`5cc816d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5cc816d0af73e20c648e044a027c589704ab1625))
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: update install instructions ([`57ee735`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/57ee735e5c2436d45a285507cdc939daa20e8e8f))
|
||||
* ci: cleanup ([`11173b9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/11173b9c0a7dc4b36e35962042e5b86407da49f1))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: moved to pyproject.toml; closes #162 ([`c86ce30`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c86ce302a964d71ee631f0817609ab5aa0e3ab0f))
|
||||
* feat: added isort to bw-generate-cli ([`f0391f5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0391f59c9eb0a51b693fccfe2e399e869d35dda))
|
||||
|
||||
* feat: added entry point for bw-generate-cli ([`1c7f491`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1c7f4912ce5998e666276969bf4af8656d619a91))
|
||||
|
||||
* feat(cli): auto-discover rpc-enabled widgets ([`df1be10`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df1be10057a5e85a3f35bef1c1b27366b6727276))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: fixed semver job and upgraded to v9 ([`32e1a9d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/32e1a9d8472eb1c25d30697d407a8ffecd04e75d))
|
||||
* fix: removed BECConnector from rpc client interface ([`6428e38`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6428e38ab94c15a2c904e75cc6404bb6d0394e04))
|
||||
|
||||
* fix: added bec_ipython_client as dependency; needed for jupyter widget ([`006a089`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/006a0894b85cba3b2773737ed6fe3e92c81cdee0))
|
||||
|
||||
* fix(BECFigure): removed duplicated user access for plot ([`954c576`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/954c576131f7deac669ddf9f51eeb1d41b6f92b7))
|
||||
|
||||
* fix(bec_connector): field validator should be a classmethod ([`867720a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/867720a897b6713bd0df9af71ffdd11a6a380f7d))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: applied formatter ([`4117fd7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4117fd7b5b2090ff4fb7ad9e0d92cc87cd13ed5f))
|
||||
* refactor: minor cleanup ([`3adf6cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3adf6cfd586355c8b8ce7fdc9722f868e22287c5))
|
||||
|
||||
* refactor: disabled pylint for auto-gen client ([`b15816c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b15816ca9fd3e4ae87cca5fcfe029b4dfca570ca))
|
||||
|
||||
## v0.52.1 (2024-05-08)
|
||||
* refactor(isort): added bec_widgets as known first party package ([`9c5a471`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9c5a471234ed2928e4527b079436db2a807c5f6f))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(docstrings): docstrings formating fixed for sphinx to properly format readdocs ([`7f2f7cd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7f2f7cd07a14876617cd83cedde8c281fdc52c3a))
|
||||
|
||||
|
||||
## v0.52.0 (2024-05-07)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: fixed support for child pipelines ([`e65c7f3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e65c7f3be895ada407bd358edf67d569d2cab08e))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(utils/layout_manager): added GridLayoutManager to extend functionalities of native QGridLayout ([`fcd6ef0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fcd6ef0975dc872f69c9d6fb2b8a1ad04a423aae))
|
||||
|
||||
* feat(widget/dock): BECDock and BECDock area for dockable windows ([`d8ff8af`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d8ff8afcd474660a6069bbdab05f10a65f221727))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(widgets/dock): BECDockArea close overwrites the default pyqtgraph Container close + minor improvements ([`ceae979`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ceae979f375ecc33c5c97148f197655c1ca57b6c))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(widget/plots): WidgetConfig changed to SubplotConfig ([`03fa1f2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/03fa1f26d0fa6b58ed05556fb2438d1e62f6c107))
|
||||
|
||||
|
||||
## v0.51.0 (2024-05-07)
|
||||
|
||||
### Build
|
||||
|
||||
* build(cli): changed repo name to bec_widgets ([`799ea55`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/799ea554de9a7f3720d100be4886a63f02c6a390))
|
||||
|
||||
* build(setup): fakeredis added to dev env ([`df32350`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df323504fea024a97304d96c2e39e61450714069))
|
||||
|
||||
* build(setup): PyQt6 version is set to 6.7 ([`0ab8aa3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0ab8aa3a2fe51b5c38b25fca44c1c422bb42478d))
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: added rule for parent-child pipelines ([`e085125`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e0851250eecb85503db929d37f75d2ba366308a6))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(utils): added plugin helper to find and load ([`5ece269`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5ece269adb0e9b0c2a468f1dfbaa6212e86d3561))
|
||||
|
||||
|
||||
## v0.50.2 (2024-04-30)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: 'disconnect_slot' has to be symmetric with 'connect_slot' regarding QtThreadSafeCallback ([`0dfcaa4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0dfcaa4b708948af0a40ec7cf34d03ff1e96ffac))
|
||||
|
||||
|
||||
## v0.50.1 (2024-04-29)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(cli): BECFigure takes the port to connect to redis from the current BECClient, supporting plugins ([`57cb136`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/57cb136a098e87a452414bf44e627edb562f6799))
|
||||
|
||||
|
||||
## v0.50.0 (2024-04-29)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(widgets/figure): access pattern changed for getting widgets by coordinates for rpc ([`13c018a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/13c018a79704a7497c140df57179d294e43ecffa))
|
||||
|
||||
* fix(plots): cleanup policy reviewed for children items ([`8f20a0b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8f20a0b3b1b5dd117b36b45645717190b9ee9cbf))
|
||||
* refactor(dock): parent_dock_area changed to orig_area (native for pyqtgraph) ([`2b40602`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2b40602bdc593ece0447ec926c2100414bd5cf67))
|
||||
|
||||
### Test
|
||||
|
||||
* test(cli/rpc_register): e2e RPCRegister ([`4f261be`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f261be4c7cfe54501443d031f9266f4f838f6e2))
|
||||
* test: added missing pylint statement to header ([`f662985`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f6629852ebc2b4ee239fa560cc310a5ae2627cf7))
|
||||
|
||||
* test(cli/rpc_register): rpc_register tests added ([`40eb75f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/40eb75f85a4d99d498b086a37e799276a9d2ac3f))
|
||||
|
||||
## v0.59.1 (2024-06-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(curve): set_color_map_z typo fixed in user access ([`e7838b0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e7838b0f2fc23b0a232ed7d68fbd7f3493a91b9e))
|
||||
|
||||
|
||||
## v0.59.0 (2024-06-07)
|
||||
|
||||
### Build
|
||||
|
||||
* build: added webengine dependency ([`d56c549`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d56c5493cd28f379d04a79d90b01c73b0760da1b))
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: merged additional tests to parallel matrix job ([`178fe4d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/178fe4d2da3a959f7cd90e7ea0f47314dc1ef4ed))
|
||||
|
||||
* ci: added webengine dependencies ([`2d79ef8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2d79ef8fe5e52c61f4a78782770377cd6b41958b))
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: added website docs ([`cf6e5a4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf6e5a40fc8320e9898a446a5bf14b77e94ef013))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(widget): added simple website widget with rpc ([`64abd67`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/64abd67b9b416bff9c89880b248d6e8639aa1e70))
|
||||
|
||||
|
||||
## v0.58.1 (2024-06-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(dock): new dock can be detached upon creation ([`02a2608`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/02a26086c4540127a11c235cba30afc4fd712007))
|
||||
|
||||
|
||||
## v0.58.0 (2024-06-07)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(utils.colors): general color validators ([`3094632`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/30946321348abc349fb4003dc39d0232dc19606c))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: bar colormap dynamic setting ([`67fd5e8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/67fd5e8581f60fe64027ac57f1f12cefa4d28343))
|
||||
|
||||
* fix: formatting isort ([`bf699ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bf699ec1fbe2aacd31854e84fb0438c336840fcf))
|
||||
|
||||
* fix(curve): 2D scatter updated if color_map_z is changed ([`6985ff0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6985ff0fcef9791b53198206ec8cbccd1d65ef99))
|
||||
|
||||
* fix(curve): color_map_z setting works ([`33f7be4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33f7be42c512402dab3fdd9781a8234e3ec5f4ba))
|
||||
|
||||
### Test
|
||||
|
||||
* test(color): validation tests added ([`c0ddece`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c0ddeceeeabacbf33019a8f24b18821926dc17ac))
|
||||
|
||||
|
||||
## v0.57.7 (2024-06-07)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: added schema of BECDockArea and BECFigure ([`828067f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/828067f486a905eb4678538df58e2bdd6c770de1))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: add model_config to pydantic models to allow runtime checks after creation ([`ca5e8d2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ca5e8d2fbbffbf221cc5472710fef81a33ee29d6))
|
||||
|
||||
|
||||
## v0.57.6 (2024-06-06)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(bar): docstrings extended ([`edb1775`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/edb1775967c3ff0723d0edad2b764f1ffc832b7c))
|
||||
|
||||
|
||||
## v0.57.5 (2024-06-06)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(figure): docs adjusted to be compatible with new signature ([`c037b87`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c037b87675af91b26e8c7c60e76622d4ed4cf5d5))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(waveform): added .plot method with the same signature as BECFigure.plot ([`8479caf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8479caf53a7325788ca264e5bd9aee01f1d4c5a0))
|
||||
|
||||
* fix(plot_base): .plot removed from plot_base.py, because there is no use case for it ([`82e2c89`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/82e2c898d2e26f786b2d481f85c647472675e75b))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(figure): logic for .add_image and .image consolidated; logic for .add_plot and .plot consolidated ([`52bc322`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/52bc322b2b8d3ef92ff3480e61bddaf32464f976))
|
||||
|
||||
|
||||
## v0.57.4 (2024-06-06)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(docks): set_title do update dock internal _name now ([`15cbc21`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/15cbc21e5bb3cf85f5822d44a2b3665b5aa2f346))
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
|
||||
# from .auto_updates import AutoUpdates, ScanInfo
|
||||
# TODO: put back when Pydantic gets faster
|
||||
AutoUpdates, ScanInfo = lazy_import_from(
|
||||
"bec_widgets.cli.auto_updates", ("AutoUpdates", "ScanInfo")
|
||||
)
|
||||
from .client import BECDockArea, BECFigure
|
||||
from .client import *
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import BECFigure
|
||||
from .client import BECDockArea, BECFigure
|
||||
|
||||
|
||||
class ScanInfo(BaseModel):
|
||||
@@ -15,12 +15,24 @@ class ScanInfo(BaseModel):
|
||||
scan_report_devices: list
|
||||
monitored_devices: list
|
||||
status: str
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class AutoUpdates:
|
||||
def __init__(self, figure: BECFigure, enabled: bool = True):
|
||||
self.enabled = enabled
|
||||
self.figure = figure
|
||||
create_default_dock: bool = False
|
||||
enabled: bool = False
|
||||
dock_name: str = None
|
||||
|
||||
def __init__(self, gui: BECDockArea):
|
||||
self.gui = gui
|
||||
|
||||
def start_default_dock(self):
|
||||
"""
|
||||
Create a default dock for the auto updates.
|
||||
"""
|
||||
dock = self.gui.add_dock("default_figure")
|
||||
dock.add_widget("BECFigure")
|
||||
self.dock_name = "default_figure"
|
||||
|
||||
@staticmethod
|
||||
def get_scan_info(msg) -> ScanInfo:
|
||||
@@ -44,6 +56,18 @@ class AutoUpdates:
|
||||
status=status,
|
||||
)
|
||||
|
||||
def get_default_figure(self) -> BECFigure | None:
|
||||
"""
|
||||
Get the default figure from the GUI.
|
||||
"""
|
||||
dock = self.gui.panels.get(self.dock_name, [])
|
||||
if not dock:
|
||||
return None
|
||||
widgets = dock.widget_list
|
||||
if not widgets:
|
||||
return None
|
||||
return widgets[0]
|
||||
|
||||
def run(self, msg):
|
||||
"""
|
||||
Run the update function if enabled.
|
||||
@@ -86,33 +110,44 @@ class AutoUpdates:
|
||||
"""
|
||||
Simple line scan.
|
||||
"""
|
||||
fig = self.get_default_figure()
|
||||
if not fig:
|
||||
return
|
||||
dev_x = info.scan_report_devices[0]
|
||||
dev_y = self.get_selected_device(info.monitored_devices, self.figure.selected_device)
|
||||
dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
|
||||
if not dev_y:
|
||||
return
|
||||
self.figure.clear_all()
|
||||
plt = self.figure.plot(dev_x, dev_y)
|
||||
fig.clear_all()
|
||||
plt = fig.plot(x_name=dev_x, y_name=dev_y, label=f"Scan {info.scan_number} - {dev_y}")
|
||||
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
|
||||
def simple_grid_scan(self, info: ScanInfo) -> None:
|
||||
"""
|
||||
Simple grid scan.
|
||||
"""
|
||||
fig = self.get_default_figure()
|
||||
if not fig:
|
||||
return
|
||||
dev_x = info.scan_report_devices[0]
|
||||
dev_y = info.scan_report_devices[1]
|
||||
dev_z = self.get_selected_device(info.monitored_devices, self.figure.selected_device)
|
||||
self.figure.clear_all()
|
||||
plt = self.figure.plot(dev_x, dev_y, dev_z, label=f"Scan {info.scan_number}")
|
||||
dev_z = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
|
||||
fig.clear_all()
|
||||
plt = fig.plot(
|
||||
x_name=dev_x, y_name=dev_y, z_name=dev_z, label=f"Scan {info.scan_number} - {dev_z}"
|
||||
)
|
||||
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
|
||||
def best_effort(self, info: ScanInfo) -> None:
|
||||
"""
|
||||
Best effort scan.
|
||||
"""
|
||||
fig = self.get_default_figure()
|
||||
if not fig:
|
||||
return
|
||||
dev_x = info.scan_report_devices[0]
|
||||
dev_y = self.get_selected_device(info.monitored_devices, self.figure.selected_device)
|
||||
dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
|
||||
if not dev_y:
|
||||
return
|
||||
self.figure.clear_all()
|
||||
plt = self.figure.plot(dev_x, dev_y, label=f"Scan {info.scan_number}")
|
||||
fig.clear_all()
|
||||
plt = fig.plot(x_name=dev_x, y_name=dev_y, label=f"Scan {info.scan_number} - {dev_y}")
|
||||
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
|
||||
@@ -14,13 +14,18 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
|
||||
from qtpy.QtCore import QCoreApplication
|
||||
from qtpy.QtCore import QEventLoop, QSocketNotifier, QTimer
|
||||
|
||||
import bec_widgets.cli.client as client
|
||||
from bec_widgets.cli.auto_updates import AutoUpdates
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.device import DeviceBase
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea, BECFigure
|
||||
|
||||
from bec_lib.serialization import MsgpackSerialization
|
||||
|
||||
messages = lazy_import("bec_lib.messages")
|
||||
# from bec_lib.connector import MessageObject
|
||||
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
|
||||
@@ -58,20 +63,67 @@ def rpc_call(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
def _get_output(process) -> None:
|
||||
try:
|
||||
os.set_blocking(process.stdout.fileno(), False)
|
||||
os.set_blocking(process.stderr.fileno(), False)
|
||||
while process.poll() is None:
|
||||
readylist, _, _ = select.select([process.stdout, process.stderr], [], [], 1)
|
||||
if process.stdout in readylist:
|
||||
output = process.stdout.read(1024)
|
||||
if output:
|
||||
print(output, end="")
|
||||
if process.stderr in readylist:
|
||||
error_output = process.stderr.read(1024)
|
||||
if error_output:
|
||||
print(error_output, end="", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Error reading process output: {str(e)}")
|
||||
|
||||
|
||||
def _start_plot_process(gui_id, gui_class, config) -> None:
|
||||
"""
|
||||
Start the plot in a new process.
|
||||
"""
|
||||
# pylint: disable=subprocess-run-check
|
||||
monitor_module = importlib.import_module("bec_widgets.cli.server")
|
||||
monitor_path = monitor_module.__file__
|
||||
|
||||
command = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
monitor_path,
|
||||
"--id",
|
||||
gui_id,
|
||||
"--config",
|
||||
config,
|
||||
"--gui_class",
|
||||
gui_class.__name__,
|
||||
]
|
||||
process = subprocess.Popen(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
process_output_processing_thread = threading.Thread(target=_get_output, args=(process,))
|
||||
process_output_processing_thread.start()
|
||||
return process, process_output_processing_thread
|
||||
|
||||
|
||||
class BECGuiClientMixin:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._process = None
|
||||
self.update_script = self._get_update_script()
|
||||
self._process_output_processing_thread = None
|
||||
self.auto_updates = self._get_update_script()
|
||||
self._target_endpoint = MessageEndpoints.scan_status()
|
||||
self._selected_device = None
|
||||
self.stderr_output = []
|
||||
|
||||
def _get_update_script(self) -> AutoUpdates:
|
||||
def _get_update_script(self) -> AutoUpdates | None:
|
||||
eps = imd.entry_points(group="bec.widgets.auto_updates")
|
||||
for ep in eps:
|
||||
if ep.name == "plugin_widgets_update":
|
||||
return ep.load()(figure=self)
|
||||
try:
|
||||
return ep.load()(gui=self)
|
||||
except Exception as e:
|
||||
print(f"Error loading auto update script from plugin: {str(e)}")
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -97,7 +149,7 @@ class BECGuiClientMixin:
|
||||
|
||||
@staticmethod
|
||||
def _handle_msg_update(msg: MessageObject, parent: BECGuiClientMixin) -> None:
|
||||
if parent.update_script is not None:
|
||||
if parent.auto_updates is not None:
|
||||
# pylint: disable=protected-access
|
||||
parent._update_script_msg_parser(msg.value)
|
||||
|
||||
@@ -105,14 +157,17 @@ class BECGuiClientMixin:
|
||||
if isinstance(msg, messages.ScanStatusMessage):
|
||||
if not self.gui_is_alive():
|
||||
return
|
||||
self.update_script.run(msg)
|
||||
self.auto_updates.run(msg)
|
||||
|
||||
def show(self) -> None:
|
||||
"""
|
||||
Show the figure.
|
||||
"""
|
||||
if self._process is None or self._process.poll() is not None:
|
||||
self._start_plot_process()
|
||||
self._start_update_script()
|
||||
self._process, self._process_output_processing_thread = _start_plot_process(
|
||||
self._gui_id, self.__class__, self._client._service_config.redis
|
||||
)
|
||||
while not self.gui_is_alive():
|
||||
print("Waiting for GUI to start...")
|
||||
time.sleep(1)
|
||||
@@ -132,34 +187,6 @@ class BECGuiClientMixin:
|
||||
self._process = None
|
||||
self._client.shutdown()
|
||||
|
||||
def _start_plot_process(self) -> None:
|
||||
"""
|
||||
Start the plot in a new process.
|
||||
"""
|
||||
self._start_update_script()
|
||||
# pylint: disable=subprocess-run-check
|
||||
config = self._client._service_config.redis
|
||||
monitor_module = importlib.import_module("bec_widgets.cli.server")
|
||||
monitor_path = monitor_module.__file__
|
||||
gui_class = self.__class__.__name__
|
||||
|
||||
command = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
monitor_path,
|
||||
"--id",
|
||||
self._gui_id,
|
||||
"--config",
|
||||
config,
|
||||
"--gui_class",
|
||||
gui_class,
|
||||
]
|
||||
self._process = subprocess.Popen(
|
||||
command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
self._process_output_processing_thread = threading.Thread(target=self._get_output)
|
||||
self._process_output_processing_thread.start()
|
||||
|
||||
def print_log(self) -> None:
|
||||
"""
|
||||
Print the log of the plot process.
|
||||
@@ -170,26 +197,6 @@ class BECGuiClientMixin:
|
||||
# Flush list
|
||||
self.stderr_output.clear()
|
||||
|
||||
def _get_output(self) -> str:
|
||||
try:
|
||||
os.set_blocking(self._process.stdout.fileno(), False)
|
||||
os.set_blocking(self._process.stderr.fileno(), False)
|
||||
while self._process.poll() is None:
|
||||
readylist, _, _ = select.select(
|
||||
[self._process.stdout, self._process.stderr], [], [], 1
|
||||
)
|
||||
if self._process.stdout in readylist:
|
||||
output = self._process.stdout.read(1024)
|
||||
if output:
|
||||
print(output, end="")
|
||||
if self._process.stderr in readylist:
|
||||
error_output = self._process.stderr.read(1024)
|
||||
if error_output:
|
||||
print(error_output, end="", file=sys.stderr)
|
||||
self.stderr_output.append(error_output)
|
||||
except Exception as e:
|
||||
print(f"Error reading process output: {str(e)}")
|
||||
|
||||
|
||||
class RPCResponseTimeoutError(Exception):
|
||||
"""Exception raised when an RPC response is not received within the expected time."""
|
||||
@@ -200,6 +207,48 @@ class RPCResponseTimeoutError(Exception):
|
||||
)
|
||||
|
||||
|
||||
class QtRedisMessageWaiter:
|
||||
def __init__(self, redis_connector, message_to_wait):
|
||||
self.ev_loop = QEventLoop()
|
||||
self.response = None
|
||||
self.connector = redis_connector
|
||||
self.message_to_wait = message_to_wait
|
||||
self.pubsub = redis_connector._redis_conn.pubsub()
|
||||
self.pubsub.subscribe(self.message_to_wait.endpoint)
|
||||
fd = self.pubsub.connection._sock.fileno()
|
||||
self.notifier = QSocketNotifier(fd, QSocketNotifier.Read)
|
||||
self.notifier.activated.connect(self._pubsub_readable)
|
||||
|
||||
def _msg_received(self, msg_obj):
|
||||
self.response = msg_obj.value
|
||||
self.ev_loop.quit()
|
||||
|
||||
def wait(self, timeout=1):
|
||||
timer = QTimer()
|
||||
timer.singleShot(timeout * 1000, self.ev_loop.quit)
|
||||
self.ev_loop.exec_()
|
||||
timer.stop()
|
||||
self.notifier.setEnabled(False)
|
||||
self.pubsub.close()
|
||||
return self.response
|
||||
|
||||
def _pubsub_readable(self, fd):
|
||||
while True:
|
||||
msg = self.pubsub.get_message()
|
||||
if msg:
|
||||
if msg["type"] == "subscribe":
|
||||
# get_message buffers, so we may already have the answer
|
||||
# let's check...
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
return
|
||||
channel = msg["channel"].decode()
|
||||
msg = MessageObject(topic=channel, value=MsgpackSerialization.loads(msg["data"]))
|
||||
self.connector._execute_callback(self._msg_received, msg, {})
|
||||
|
||||
|
||||
class RPCBase:
|
||||
def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
|
||||
self._client = BECDispatcher().client
|
||||
@@ -226,7 +275,7 @@ class RPCBase:
|
||||
parent = parent._parent
|
||||
return parent
|
||||
|
||||
def _run_rpc(self, method, *args, wait_for_rpc_response=True, **kwargs):
|
||||
def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=3, **kwargs):
|
||||
"""
|
||||
Run the RPC call.
|
||||
|
||||
@@ -248,16 +297,24 @@ class RPCBase:
|
||||
|
||||
# pylint: disable=protected-access
|
||||
receiver = self._root._gui_id
|
||||
if wait_for_rpc_response:
|
||||
redis_msg = QtRedisMessageWaiter(
|
||||
self._client.connector, MessageEndpoints.gui_instruction_response(request_id)
|
||||
)
|
||||
|
||||
self._client.connector.set_and_publish(MessageEndpoints.gui_instructions(receiver), rpc_msg)
|
||||
|
||||
if not wait_for_rpc_response:
|
||||
return None
|
||||
response = self._wait_for_response(request_id)
|
||||
# get class name
|
||||
if not response.content["accepted"]:
|
||||
raise ValueError(response.content["message"]["error"])
|
||||
msg_result = response.content["message"].get("result")
|
||||
return self._create_widget_from_msg_result(msg_result)
|
||||
if wait_for_rpc_response:
|
||||
response = redis_msg.wait(timeout)
|
||||
|
||||
if response is None:
|
||||
raise RPCResponseTimeoutError(request_id, timeout)
|
||||
|
||||
# get class name
|
||||
if not response.accepted:
|
||||
raise ValueError(response.message["error"])
|
||||
msg_result = response.message.get("result")
|
||||
return self._create_widget_from_msg_result(msg_result)
|
||||
|
||||
def _create_widget_from_msg_result(self, msg_result):
|
||||
if msg_result is None:
|
||||
@@ -280,30 +337,6 @@ class RPCBase:
|
||||
return cls(parent=self, **msg_result)
|
||||
return msg_result
|
||||
|
||||
def _wait_for_response(self, request_id: str, timeout: int = 5):
|
||||
"""
|
||||
Wait for the response from the server.
|
||||
|
||||
Args:
|
||||
request_id(str): The request ID.
|
||||
timeout(int): The timeout in seconds.
|
||||
|
||||
Returns:
|
||||
The response from the server.
|
||||
"""
|
||||
start_time = time.time()
|
||||
response = None
|
||||
|
||||
while response is None and self.gui_is_alive() and (time.time() - start_time) < timeout:
|
||||
response = self._client.connector.get(
|
||||
MessageEndpoints.gui_instruction_response(request_id)
|
||||
)
|
||||
QCoreApplication.processEvents() # keep UI responsive (and execute signals/slots)
|
||||
if response is None and (time.time() - start_time) >= timeout:
|
||||
raise RPCResponseTimeoutError(request_id, timeout)
|
||||
|
||||
return response
|
||||
|
||||
def gui_is_alive(self):
|
||||
"""
|
||||
Check if the GUI is alive.
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
# pylint: disable=missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
import black
|
||||
import isort
|
||||
from qtpy.QtWidgets import QGraphicsWidget, QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import get_overloads
|
||||
@@ -14,30 +22,52 @@ else:
|
||||
"If you want to use the real function 'typing.get_overloads()', please use Python 3.11 or later."
|
||||
)
|
||||
|
||||
def get_overloads(obj):
|
||||
# Dummy function for Python versions before 3.11
|
||||
def get_overloads(_obj):
|
||||
"""
|
||||
Dummy function for Python versions before 3.11.
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class ClientGenerator:
|
||||
def __init__(self):
|
||||
self.header = """# This file was automatically generated by generate_cli.py\n
|
||||
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECGuiClientMixin
|
||||
from typing import Literal, Optional, overload"""
|
||||
import enum
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
from bec_widgets.cli.client_utils import RPCBase, rpc_call, BECGuiClientMixin
|
||||
|
||||
# pylint: skip-file"""
|
||||
|
||||
self.content = ""
|
||||
|
||||
def generate_client(self, published_classes: list):
|
||||
def generate_client(
|
||||
self, published_classes: dict[Literal["connector_classes", "top_level_classes"], list[type]]
|
||||
):
|
||||
"""
|
||||
Generate the client for the published classes.
|
||||
|
||||
Args:
|
||||
published_classes(list): The list of published classes (e.g. [BECWaveform1D, BECFigure]).
|
||||
published_classes(dict): A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
|
||||
"""
|
||||
for cls in published_classes:
|
||||
self.write_client_enum(published_classes["top_level_classes"])
|
||||
for cls in published_classes["connector_classes"]:
|
||||
self.content += "\n\n"
|
||||
self.generate_content_for_class(cls)
|
||||
|
||||
def write_client_enum(self, published_classes: list[type]):
|
||||
"""
|
||||
Write the client enum to the content.
|
||||
"""
|
||||
self.content += """
|
||||
class Widgets(str, enum.Enum):
|
||||
\"\"\"
|
||||
Enum for the available widgets.
|
||||
\"\"\"
|
||||
"""
|
||||
for cls in published_classes:
|
||||
self.content += f'{cls.__name__} = "{cls.__name__}"\n '
|
||||
|
||||
def generate_content_for_class(self, cls):
|
||||
"""
|
||||
Generate the content for the class.
|
||||
@@ -47,11 +77,6 @@ from typing import Literal, Optional, overload"""
|
||||
"""
|
||||
|
||||
class_name = cls.__name__
|
||||
module = cls.__module__
|
||||
|
||||
# Generate the header
|
||||
# self.header += f"""
|
||||
# from {module} import {class_name}"""
|
||||
|
||||
# Generate the content
|
||||
if cls.__name__ == "BECDockArea":
|
||||
@@ -101,39 +126,85 @@ class {class_name}(RPCBase):"""
|
||||
except black.NothingChanged:
|
||||
formatted_content = full_content
|
||||
|
||||
isort.Config(
|
||||
profile="black",
|
||||
line_length=100,
|
||||
multi_line_output=3,
|
||||
include_trailing_comma=True,
|
||||
known_first_party=["bec_widgets"],
|
||||
)
|
||||
formatted_content = isort.code(formatted_content)
|
||||
|
||||
with open(file_name, "w", encoding="utf-8") as file:
|
||||
file.write(formatted_content)
|
||||
|
||||
@staticmethod
|
||||
def get_rpc_classes(
|
||||
repo_name: str,
|
||||
) -> dict[Literal["connector_classes", "top_level_classes"], list[type]]:
|
||||
"""
|
||||
Get all RPC-enabled classes in the specified repository.
|
||||
|
||||
Args:
|
||||
repo_name(str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
|
||||
"""
|
||||
connector_classes = []
|
||||
top_level_classes = []
|
||||
anchor_module = importlib.import_module(f"{repo_name}.widgets")
|
||||
directory = os.path.dirname(anchor_module.__file__)
|
||||
for root, _, files in sorted(os.walk(directory)):
|
||||
for file in files:
|
||||
if not file.endswith(".py") or file.startswith("__"):
|
||||
continue
|
||||
|
||||
path = os.path.join(root, file)
|
||||
subs = os.path.dirname(os.path.relpath(path, directory)).split("/")
|
||||
if len(subs) == 1 and not subs[0]:
|
||||
module_name = file.split(".")[0]
|
||||
else:
|
||||
module_name = ".".join(subs + [file.split(".")[0]])
|
||||
|
||||
module = importlib.import_module(f"{repo_name}.widgets.{module_name}")
|
||||
|
||||
for name in dir(module):
|
||||
obj = getattr(module, name)
|
||||
if not hasattr(obj, "__module__") or obj.__module__ != module.__name__:
|
||||
continue
|
||||
if isinstance(obj, type) and issubclass(obj, BECConnector):
|
||||
connector_classes.append(obj)
|
||||
if len(subs) == 1 and (
|
||||
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
|
||||
):
|
||||
top_level_classes.append(obj)
|
||||
|
||||
return {"connector_classes": connector_classes, "top_level_classes": top_level_classes}
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for the script, controlled by command line arguments.
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(description="Auto-generate the client for RPC widgets")
|
||||
parser.add_argument("--core", action="store_true", help="Whether to generate the core client")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.core:
|
||||
current_path = os.path.dirname(__file__)
|
||||
client_path = os.path.join(current_path, "client.py")
|
||||
|
||||
rpc_classes = ClientGenerator.get_rpc_classes("bec_widgets")
|
||||
rpc_classes["connector_classes"].sort(key=lambda x: x.__name__)
|
||||
|
||||
generator = ClientGenerator()
|
||||
generator.generate_client(rpc_classes)
|
||||
generator.write(client_path)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import os
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.widgets import BECDock, BECDockArea, BECFigure, SpiralProgressBar
|
||||
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
|
||||
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem
|
||||
from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform import BECWaveform
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import BECCurve
|
||||
from bec_widgets.widgets.spiral_progress_bar.ring import Ring
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
client_path = os.path.join(current_path, "client.py")
|
||||
clss = [
|
||||
BECPlotBase,
|
||||
BECWaveform,
|
||||
BECFigure,
|
||||
BECCurve,
|
||||
BECImageShow,
|
||||
BECConnector,
|
||||
BECImageItem,
|
||||
BECMotorMap,
|
||||
BECDock,
|
||||
BECDockArea,
|
||||
SpiralProgressBar,
|
||||
Ring,
|
||||
]
|
||||
generator = ClientGenerator()
|
||||
generator.generate_client(clss)
|
||||
generator.write(client_path)
|
||||
sys.argv = ["generate_cli.py", "--core"]
|
||||
main()
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBar
|
||||
from bec_widgets.widgets.website.website import WebsiteWidget
|
||||
|
||||
|
||||
class RPCWidgetHandler:
|
||||
"""Handler class for creating widgets from RPC messages."""
|
||||
|
||||
widget_classes = {"BECFigure": BECFigure, "SpiralProgressBar": SpiralProgressBar}
|
||||
widget_classes = {
|
||||
"BECFigure": BECFigure,
|
||||
"SpiralProgressBar": SpiralProgressBar,
|
||||
"Website": WebsiteWidget,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_widget(widget_type, **kwargs) -> BECConnector:
|
||||
|
||||
@@ -12,22 +12,22 @@ from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
from bec_widgets.utils import BECDispatcher, UILoader
|
||||
from bec_widgets.widgets import BECFigure
|
||||
from bec_widgets.widgets.dock.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.jupyter_console.jupyter_console import BECJupyterConsole
|
||||
|
||||
|
||||
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.kernel_manager = QtInProcessKernelManager()
|
||||
self.kernel_manager.start_kernel(show_banner=False)
|
||||
self.kernel_client = self.kernel_manager.client()
|
||||
self.kernel_client.start_channels()
|
||||
|
||||
self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
|
||||
|
||||
def shutdown_kernel(self):
|
||||
self.kernel_client.stop_channels()
|
||||
self.kernel_manager.shutdown_kernel()
|
||||
# class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
||||
# def __init__(self):
|
||||
# super().__init__()
|
||||
#
|
||||
# self.kernel_manager = QtInProcessKernelManager()
|
||||
# self.kernel_manager.start_kernel(show_banner=False)
|
||||
# self.kernel_client = self.kernel_manager.client()
|
||||
# self.kernel_client.start_channels()
|
||||
#
|
||||
# self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
|
||||
#
|
||||
# def shutdown_kernel(self):
|
||||
# self.kernel_client.stop_channels()
|
||||
# self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
@@ -45,25 +45,25 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self.safe_close = False
|
||||
|
||||
# console push
|
||||
self.console.kernel_manager.kernel.shell.push(
|
||||
{
|
||||
"fig": self.figure,
|
||||
"dock": self.dock,
|
||||
"w1": self.w1,
|
||||
"w2": self.w2,
|
||||
"w3": self.w3,
|
||||
"d0": self.d0,
|
||||
"d1": self.d1,
|
||||
"d2": self.d2,
|
||||
"fig0": self.fig0,
|
||||
"fig1": self.fig1,
|
||||
"fig2": self.fig2,
|
||||
"bar": self.bar,
|
||||
"bec": self.figure.client,
|
||||
"scans": self.figure.client.scans,
|
||||
"dev": self.figure.client.device_manager.devices,
|
||||
}
|
||||
)
|
||||
if self.console.inprocess is True:
|
||||
self.console.kernel_manager.kernel.shell.push(
|
||||
{
|
||||
"np": np,
|
||||
"pg": pg,
|
||||
"fig": self.figure,
|
||||
"dock": self.dock,
|
||||
"w1": self.w1,
|
||||
"w2": self.w2,
|
||||
"w3": self.w3,
|
||||
"d0": self.d0,
|
||||
"d1": self.d1,
|
||||
"d2": self.d2,
|
||||
"fig0": self.fig0,
|
||||
"fig1": self.fig1,
|
||||
"fig2": self.fig2,
|
||||
"bar": self.bar,
|
||||
}
|
||||
)
|
||||
|
||||
def _init_ui(self):
|
||||
# Plotting window
|
||||
@@ -82,14 +82,14 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self._init_dock()
|
||||
|
||||
self.console_layout = QVBoxLayout(self.ui.widget_console)
|
||||
self.console = JupyterConsoleWidget()
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
self.console_layout.addWidget(self.console)
|
||||
self.console.set_default_style("linux")
|
||||
|
||||
def _init_figure(self):
|
||||
self.figure.plot(x_name="samx", y_name="bpm4d")
|
||||
self.figure.plot(x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="cividis")
|
||||
self.figure.motor_map("samx", "samy")
|
||||
self.figure.image("eiger", color_map="viridis", vrange=(0, 100))
|
||||
self.figure.add_plot(x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="magma")
|
||||
|
||||
self.figure.change_layout(2, 2)
|
||||
|
||||
@@ -98,26 +98,25 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self.w3 = self.figure[1, 0]
|
||||
|
||||
# curves for w1
|
||||
self.w1.add_curve_scan("samx", "samy", "bpm4i", pen_style="dash")
|
||||
self.w1.add_curve_scan("samx", "samy", "bpm3a", pen_style="dash")
|
||||
self.c1 = self.w1.get_config()
|
||||
|
||||
def _init_dock(self):
|
||||
|
||||
self.d0 = self.dock.add_dock(name="dock_0")
|
||||
self.fig0 = self.d0.add_widget_bec("BECFigure")
|
||||
self.fig0 = self.d0.add_widget("BECFigure")
|
||||
data = np.random.rand(10, 2)
|
||||
self.fig0.plot(data, label="2d Data")
|
||||
self.fig0.image("eiger", vrange=(0, 100))
|
||||
|
||||
self.d1 = self.dock.add_dock(name="dock_1", position="right")
|
||||
self.fig1 = self.d1.add_widget_bec("BECFigure")
|
||||
self.fig1 = self.d1.add_widget("BECFigure")
|
||||
self.fig1.plot(x_name="samx", y_name="bpm4i")
|
||||
self.fig1.plot(x_name="samx", y_name="bpm3a")
|
||||
|
||||
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
|
||||
self.fig2 = self.d2.add_widget_bec("BECFigure", row=0, col=0)
|
||||
self.fig2.motor_map(x_name="samx", y_name="samy")
|
||||
self.fig2 = self.d2.add_widget("BECFigure", row=0, col=0)
|
||||
self.fig2.plot(x_name="samx", y_name="bpm4i")
|
||||
self.bar = self.d2.add_widget_bec("SpiralProgressBar", row=0, col=1)
|
||||
self.bar = self.d2.add_widget("SpiralProgressBar", row=0, col=1)
|
||||
self.bar.set_diameter(200)
|
||||
|
||||
self.dock.save_state()
|
||||
@@ -137,10 +136,6 @@ if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
module_path = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Jupyter Console")
|
||||
app.setApplicationDisplayName("Jupyter Console")
|
||||
@@ -148,6 +143,11 @@ if __name__ == "__main__": # pragma: no cover
|
||||
icon = QIcon()
|
||||
icon.addFile(os.path.join(module_path, "assets", "terminal_icon.png"), size=QSize(48, 48))
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
win = JupyterConsoleWindow()
|
||||
win.show()
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ class ConnectionConfig(BaseModel):
|
||||
gui_id: Optional[str] = Field(
|
||||
default=None, validate_default=True, description="The GUI ID of the widget."
|
||||
)
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
@field_validator("gui_id")
|
||||
@classmethod
|
||||
def generate_gui_id(cls, v, values):
|
||||
"""Generate a GUI ID if none is provided."""
|
||||
if v is None:
|
||||
|
||||
@@ -9,7 +9,7 @@ import redis
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.redis_connector import MessageObject, RedisConnector
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import QCoreApplication, QObject
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -71,6 +71,7 @@ class BECDispatcher:
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
qapp = None
|
||||
|
||||
def __new__(cls, client=None, config: str = None, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
@@ -82,6 +83,9 @@ class BECDispatcher:
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
if not QCoreApplication.instance():
|
||||
BECDispatcher.qapp = QCoreApplication([])
|
||||
|
||||
self._slots = collections.defaultdict(set)
|
||||
self.client = client
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import re
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy.QtGui import QColor
|
||||
|
||||
|
||||
class Colors:
|
||||
|
||||
@staticmethod
|
||||
def golden_ratio(num: int) -> list:
|
||||
"""Calculate the golden ratio for a given number of angles.
|
||||
@@ -63,3 +66,211 @@ class Colors:
|
||||
else:
|
||||
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
|
||||
return colors
|
||||
|
||||
@staticmethod
|
||||
def validate_color(color: tuple | str) -> tuple | str:
|
||||
"""
|
||||
Validate the color input if it is HEX or RGBA compatible. Can be used in any pydantic model as a field validator.
|
||||
|
||||
Args:
|
||||
color(tuple|str): The color to be validated. Can be a tuple of RGBA values or a HEX string.
|
||||
|
||||
Returns:
|
||||
tuple|str: The validated color.
|
||||
"""
|
||||
CSS_COLOR_NAMES = {
|
||||
"aliceblue",
|
||||
"antiquewhite",
|
||||
"aqua",
|
||||
"aquamarine",
|
||||
"azure",
|
||||
"beige",
|
||||
"bisque",
|
||||
"black",
|
||||
"blanchedalmond",
|
||||
"blue",
|
||||
"blueviolet",
|
||||
"brown",
|
||||
"burlywood",
|
||||
"cadetblue",
|
||||
"chartreuse",
|
||||
"chocolate",
|
||||
"coral",
|
||||
"cornflowerblue",
|
||||
"cornsilk",
|
||||
"crimson",
|
||||
"cyan",
|
||||
"darkblue",
|
||||
"darkcyan",
|
||||
"darkgoldenrod",
|
||||
"darkgray",
|
||||
"darkgreen",
|
||||
"darkgrey",
|
||||
"darkkhaki",
|
||||
"darkmagenta",
|
||||
"darkolivegreen",
|
||||
"darkorange",
|
||||
"darkorchid",
|
||||
"darkred",
|
||||
"darksalmon",
|
||||
"darkseagreen",
|
||||
"darkslateblue",
|
||||
"darkslategray",
|
||||
"darkslategrey",
|
||||
"darkturquoise",
|
||||
"darkviolet",
|
||||
"deeppink",
|
||||
"deepskyblue",
|
||||
"dimgray",
|
||||
"dimgrey",
|
||||
"dodgerblue",
|
||||
"firebrick",
|
||||
"floralwhite",
|
||||
"forestgreen",
|
||||
"fuchsia",
|
||||
"gainsboro",
|
||||
"ghostwhite",
|
||||
"gold",
|
||||
"goldenrod",
|
||||
"gray",
|
||||
"green",
|
||||
"greenyellow",
|
||||
"grey",
|
||||
"honeydew",
|
||||
"hotpink",
|
||||
"indianred",
|
||||
"indigo",
|
||||
"ivory",
|
||||
"khaki",
|
||||
"lavender",
|
||||
"lavenderblush",
|
||||
"lawngreen",
|
||||
"lemonchiffon",
|
||||
"lightblue",
|
||||
"lightcoral",
|
||||
"lightcyan",
|
||||
"lightgoldenrodyellow",
|
||||
"lightgray",
|
||||
"lightgreen",
|
||||
"lightgrey",
|
||||
"lightpink",
|
||||
"lightsalmon",
|
||||
"lightseagreen",
|
||||
"lightskyblue",
|
||||
"lightslategray",
|
||||
"lightslategrey",
|
||||
"lightsteelblue",
|
||||
"lightyellow",
|
||||
"lime",
|
||||
"limegreen",
|
||||
"linen",
|
||||
"magenta",
|
||||
"maroon",
|
||||
"mediumaquamarine",
|
||||
"mediumblue",
|
||||
"mediumorchid",
|
||||
"mediumpurple",
|
||||
"mediumseagreen",
|
||||
"mediumslateblue",
|
||||
"mediumspringgreen",
|
||||
"mediumturquoise",
|
||||
"mediumvioletred",
|
||||
"midnightblue",
|
||||
"mintcream",
|
||||
"mistyrose",
|
||||
"moccasin",
|
||||
"navajowhite",
|
||||
"navy",
|
||||
"oldlace",
|
||||
"olive",
|
||||
"olivedrab",
|
||||
"orange",
|
||||
"orangered",
|
||||
"orchid",
|
||||
"palegoldenrod",
|
||||
"palegreen",
|
||||
"paleturquoise",
|
||||
"palevioletred",
|
||||
"papayawhip",
|
||||
"peachpuff",
|
||||
"peru",
|
||||
"pink",
|
||||
"plum",
|
||||
"powderblue",
|
||||
"purple",
|
||||
"red",
|
||||
"rosybrown",
|
||||
"royalblue",
|
||||
"saddlebrown",
|
||||
"salmon",
|
||||
"sandybrown",
|
||||
"seagreen",
|
||||
"seashell",
|
||||
"sienna",
|
||||
"silver",
|
||||
"skyblue",
|
||||
"slateblue",
|
||||
"slategray",
|
||||
"slategrey",
|
||||
"snow",
|
||||
"springgreen",
|
||||
"steelblue",
|
||||
"tan",
|
||||
"teal",
|
||||
"thistle",
|
||||
"tomato",
|
||||
"turquoise",
|
||||
"violet",
|
||||
"wheat",
|
||||
"white",
|
||||
"whitesmoke",
|
||||
"yellow",
|
||||
"yellowgreen",
|
||||
}
|
||||
if isinstance(color, str):
|
||||
hex_pattern = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
|
||||
if hex_pattern.match(color):
|
||||
return color
|
||||
elif color.lower() in CSS_COLOR_NAMES:
|
||||
return color
|
||||
else:
|
||||
raise PydanticCustomError(
|
||||
"unsupported color",
|
||||
"The color must be a valid HEX string or CSS Color.",
|
||||
{"wrong_value": color},
|
||||
)
|
||||
elif isinstance(color, tuple):
|
||||
if len(color) != 4:
|
||||
raise PydanticCustomError(
|
||||
"unsupported color",
|
||||
"The color must be a tuple of 4 elements (R, G, B, A).",
|
||||
{"wrong_value": color},
|
||||
)
|
||||
for value in color:
|
||||
if not 0 <= value <= 255:
|
||||
raise PydanticCustomError(
|
||||
"unsupported color",
|
||||
f"The color values must be between 0 and 255 in RGBA format (R,G,B,A)",
|
||||
{"wrong_value": color},
|
||||
)
|
||||
return color
|
||||
|
||||
@staticmethod
|
||||
def validate_color_map(color_map: str) -> str:
|
||||
"""
|
||||
Validate the colormap input if it is supported by pyqtgraph. Can be used in any pydantic model as a field validator. If validation fails it prints all available colormaps from pyqtgraph instance.
|
||||
|
||||
Args:
|
||||
color_map(str): The colormap to be validated.
|
||||
|
||||
Returns:
|
||||
str: The validated colormap.
|
||||
"""
|
||||
available_colormaps = pg.colormap.listMaps()
|
||||
if color_map not in available_colormaps:
|
||||
raise PydanticCustomError(
|
||||
"unsupported colormap",
|
||||
f"Colormap '{color_map}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.",
|
||||
{"wrong_value": color_map},
|
||||
)
|
||||
return color_map
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .buttons import StopButton
|
||||
from .dock import BECDock, BECDockArea
|
||||
from .figure import BECFigure, FigureConfig
|
||||
from .scan_control import ScanControl
|
||||
|
||||
1
bec_widgets/widgets/buttons/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .stop_button.stop_button import StopButton
|
||||
0
bec_widgets/widgets/buttons/stop_button/__init__.py
Normal file
32
bec_widgets/widgets/buttons/stop_button/stop_button.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
|
||||
class StopButton(BECConnector, QPushButton):
|
||||
"""A button that stops the current scan."""
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QPushButton.__init__(self, parent=parent)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
self.setText("Stop")
|
||||
self.setStyleSheet("background-color: #cc181e; color: white")
|
||||
self.clicked.connect(self.stop_scan)
|
||||
|
||||
def stop_scan(self):
|
||||
"""Stop the scan."""
|
||||
self.queue.request_scan_abortion()
|
||||
self.queue.request_queue_reset()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = StopButton()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
0
bec_widgets/widgets/console/__init__.py
Normal file
159
bec_widgets/widgets/console/console.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
import termqt
|
||||
from qtpy.QtCore import QSocketNotifier, Qt
|
||||
from qtpy.QtGui import QFont
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QWidget
|
||||
from termqt import Terminal
|
||||
|
||||
try:
|
||||
from qtpy.QtCore import pyqtRemoveInputHook
|
||||
|
||||
pyqtRemoveInputHook()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if platform.system() in ["Linux", "Darwin"]:
|
||||
terminal_cmd = os.environ["SHELL"]
|
||||
|
||||
from termqt import TerminalPOSIXExecIO
|
||||
|
||||
class TerminalExecIO(TerminalPOSIXExecIO):
|
||||
def _read_loop(self):
|
||||
pass
|
||||
|
||||
def find_utf8_split(self, data):
|
||||
"""UTF-8 characters can be 1-4 bytes long, this finds first index which is not mid character
|
||||
|
||||
Character lengths include:
|
||||
1 Bytes: 0xxxxxxx
|
||||
2 Bytes: 110xxxxx 10xxxxxx
|
||||
3 Bytes: 1110xxxx 10xxxxxx 10xxxxxx
|
||||
4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
||||
Source: https://en.wikipedia.org/wiki/UTF-8#Encoding
|
||||
|
||||
Start at end of chunk moving backwards, find first UTF-8 start byte:
|
||||
1 Bytes: 0xxxxxxx - 0x80 == 0x00
|
||||
2 Bytes: 110xxxxx - 0xE0 == 0xC0
|
||||
3 Bytes: 1110xxxx - 0xF0 == 0xE0
|
||||
4 bytes: 11110xxx - 0xF8 == 0xF0
|
||||
|
||||
Parameters:
|
||||
data (bytes) - buffer to be evaluated
|
||||
|
||||
Returns:
|
||||
(int) - last position of complete UTF-8 character
|
||||
"""
|
||||
pos = 0
|
||||
for i, c in enumerate(reversed(data)):
|
||||
if c & 0x80 == 0x00 or c & 0xE0 == 0xC0 or c & 0xF0 == 0xE0 or c & 0xF8 == 0xF0:
|
||||
pos = i
|
||||
break
|
||||
return len(data) - pos
|
||||
|
||||
def _read(self, fd):
|
||||
try:
|
||||
data = os.read(fd, 2**16) # read as much as possible
|
||||
except OSError:
|
||||
data = b""
|
||||
|
||||
if data:
|
||||
self._read_buf += data
|
||||
i = self.find_utf8_split(self._read_buf)
|
||||
output = self._read_buf[:i]
|
||||
self._read_buf = self._read_buf[i:]
|
||||
self.stdout_callback(output)
|
||||
else:
|
||||
self.logger.info("Spawned process has been killed")
|
||||
if self.running:
|
||||
self.running = False
|
||||
self.terminated_callback()
|
||||
os.close(fd)
|
||||
|
||||
def spawn(self):
|
||||
super().spawn()
|
||||
self._read_notifier = QSocketNotifier(self.fd, QSocketNotifier.Read)
|
||||
self._read_notifier.activated.connect(self._read)
|
||||
|
||||
def write(self, buffer):
|
||||
# same as original method, but without logging and without assert (unneeded)
|
||||
if not self.running:
|
||||
return
|
||||
try:
|
||||
os.write(self.fd, buffer)
|
||||
except OSError:
|
||||
self.running = False
|
||||
self.terminated_callback()
|
||||
|
||||
else:
|
||||
terminal_cmd = "cmd.exe"
|
||||
from termqt import TerminalWinptyIO as TerminalExecIO
|
||||
|
||||
|
||||
class TerminalWidget(QWidget):
|
||||
def __init__(self, logger):
|
||||
super().__init__()
|
||||
self.logger = logger
|
||||
self.terminal = Terminal(800, 600, logger=self.logger)
|
||||
self.terminal.set_font()
|
||||
self.terminal.maximum_line_history = 2000
|
||||
self.scroll = QScrollBar(Qt.Vertical, self.terminal)
|
||||
self.terminal.connect_scroll_bar(self.scroll)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(self.terminal)
|
||||
layout.addWidget(self.scroll)
|
||||
layout.setSpacing(0)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
class BECConsole(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle(f"termqt on {platform.system()}")
|
||||
self.logger = self.setup_logger()
|
||||
|
||||
self.terminal_widget = TerminalWidget(self.logger)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(self.terminal_widget)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.auto_wrap_enabled = True
|
||||
self.platform = platform.system()
|
||||
|
||||
self.setup_terminal_io()
|
||||
|
||||
def setup_logger(self):
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter("[%(asctime)s] > [%(filename)s:%(lineno)d] %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
return logger
|
||||
|
||||
def setup_terminal_io(self):
|
||||
self.terminal_io = TerminalExecIO(
|
||||
self.terminal_widget.terminal.row_len,
|
||||
self.terminal_widget.terminal.col_len,
|
||||
terminal_cmd,
|
||||
logger=self.logger,
|
||||
)
|
||||
self.auto_wrap_enabled = False
|
||||
|
||||
self.terminal_widget.terminal.enable_auto_wrap(self.auto_wrap_enabled)
|
||||
self.terminal_io.stdout_callback = self.terminal_widget.terminal.stdout
|
||||
self.terminal_widget.terminal.stdin_callback = self.terminal_io.write
|
||||
self.terminal_widget.terminal.resize_callback = self.terminal_io.resize
|
||||
self.terminal_io.spawn()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
main_window = BECConsole()
|
||||
main_window.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional
|
||||
|
||||
from pydantic import Field
|
||||
from pyqtgraph.dockarea import Dock
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class DockConfig(ConnectionConfig):
|
||||
widgets: dict[str, ConnectionConfig] = Field({}, description="The widgets in the dock.")
|
||||
widgets: dict[str, Any] = Field({}, description="The widgets in the dock.")
|
||||
position: Literal["bottom", "top", "left", "right", "above", "below"] = Field(
|
||||
"bottom", description="The position of the dock."
|
||||
)
|
||||
@@ -26,13 +26,14 @@ class DockConfig(ConnectionConfig):
|
||||
|
||||
class BECDock(BECConnector, Dock):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"rpc_id",
|
||||
"widget_list",
|
||||
"show_title_bar",
|
||||
"hide_title_bar",
|
||||
"get_widgets_positions",
|
||||
"set_title",
|
||||
"add_widget_bec",
|
||||
"add_widget",
|
||||
"list_eligible_widgets",
|
||||
"move_widget",
|
||||
"remove_widget",
|
||||
@@ -62,8 +63,6 @@ class BECDock(BECConnector, Dock):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
Dock.__init__(self, name=name, **kwargs)
|
||||
|
||||
self.parent_dock_area = parent_dock_area
|
||||
|
||||
# Layout Manager
|
||||
self.layout_manager = GridLayoutManager(self.layout)
|
||||
|
||||
@@ -72,8 +71,8 @@ class BECDock(BECConnector, Dock):
|
||||
old_area = source.area
|
||||
self.setOrientation("horizontal", force=True)
|
||||
super().dropEvent(event)
|
||||
if old_area in self.parent_dock_area.tempAreas and old_area != self.parent_dock_area:
|
||||
self.parent_dock_area.removeTempArea(old_area)
|
||||
if old_area in self.orig_area.tempAreas and old_area != self.orig_area:
|
||||
self.orig_area.removeTempArea(old_area)
|
||||
|
||||
def float(self):
|
||||
"""
|
||||
@@ -92,7 +91,7 @@ class BECDock(BECConnector, Dock):
|
||||
super().float()
|
||||
|
||||
@property
|
||||
def widget_list(self) -> list:
|
||||
def widget_list(self) -> list[BECConnector]:
|
||||
"""
|
||||
Get the widgets in the dock.
|
||||
|
||||
@@ -102,7 +101,7 @@ class BECDock(BECConnector, Dock):
|
||||
return self.widgets
|
||||
|
||||
@widget_list.setter
|
||||
def widget_list(self, value: list):
|
||||
def widget_list(self, value: list[BECConnector]):
|
||||
self.widgets = value
|
||||
|
||||
def hide_title_bar(self):
|
||||
@@ -128,8 +127,9 @@ class BECDock(BECConnector, Dock):
|
||||
Args:
|
||||
title(str): The title of the dock.
|
||||
"""
|
||||
self.parent_dock_area.docks[title] = self.parent_dock_area.docks.pop(self.name())
|
||||
self.orig_area.docks[title] = self.orig_area.docks.pop(self.name())
|
||||
self.setTitle(title)
|
||||
self._name = title
|
||||
|
||||
def get_widgets_positions(self) -> dict:
|
||||
"""
|
||||
@@ -151,46 +151,15 @@ class BECDock(BECConnector, Dock):
|
||||
"""
|
||||
return list(RPCWidgetHandler.widget_classes.keys())
|
||||
|
||||
def add_widget_bec(
|
||||
self,
|
||||
widget_type: str,
|
||||
row=None,
|
||||
col=0,
|
||||
rowspan=1,
|
||||
colspan=1,
|
||||
shift: Literal["down", "up", "left", "right"] = "down",
|
||||
):
|
||||
"""
|
||||
Add a widget to the dock.
|
||||
|
||||
Args:
|
||||
widget_type(str): The widget to add. Only BEC RPC widgets from RPCWidgetHandler are allowed.
|
||||
row(int): The row to add the widget to. If None, the widget will be added to the next available row.
|
||||
col(int): The column to add the widget to.
|
||||
rowspan(int): The number of rows the widget should span.
|
||||
colspan(int): The number of columns the widget should span.
|
||||
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
|
||||
"""
|
||||
if row is None:
|
||||
row = self.layout.rowCount()
|
||||
|
||||
if self.layout_manager.is_position_occupied(row, col):
|
||||
self.layout_manager.shift_widgets(shift, start_row=row)
|
||||
|
||||
widget = RPCWidgetHandler.create_widget(widget_type)
|
||||
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
|
||||
return widget
|
||||
|
||||
def add_widget(
|
||||
self,
|
||||
widget: QWidget,
|
||||
widget: BECConnector | str,
|
||||
row=None,
|
||||
col=0,
|
||||
rowspan=1,
|
||||
colspan=1,
|
||||
shift: Literal["down", "up", "left", "right"] = "down",
|
||||
):
|
||||
) -> BECConnector:
|
||||
"""
|
||||
Add a widget to the dock.
|
||||
|
||||
@@ -208,8 +177,18 @@ class BECDock(BECConnector, Dock):
|
||||
if self.layout_manager.is_position_occupied(row, col):
|
||||
self.layout_manager.shift_widgets(shift, start_row=row)
|
||||
|
||||
if isinstance(widget, str):
|
||||
widget = RPCWidgetHandler.create_widget(widget)
|
||||
else:
|
||||
widget = widget
|
||||
|
||||
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
|
||||
if hasattr(widget, "config"):
|
||||
self.config.widgets[widget.gui_id] = widget.config
|
||||
|
||||
return widget
|
||||
|
||||
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
|
||||
"""
|
||||
Move a widget to a new position in the layout.
|
||||
@@ -225,7 +204,7 @@ class BECDock(BECConnector, Dock):
|
||||
"""
|
||||
Attach the dock to the parent dock area.
|
||||
"""
|
||||
self.parent_dock_area.removeTempArea(self.area)
|
||||
self.orig_area.removeTempArea(self.area)
|
||||
|
||||
def detach(self):
|
||||
"""
|
||||
@@ -242,6 +221,7 @@ class BECDock(BECConnector, Dock):
|
||||
"""
|
||||
widget = self.rpc_register.get_rpc_by_id(widget_rpc_id)
|
||||
self.layout.removeWidget(widget)
|
||||
self.config.widgets.pop(widget_rpc_id, None)
|
||||
widget.close()
|
||||
|
||||
def remove(self):
|
||||
@@ -249,7 +229,7 @@ class BECDock(BECConnector, Dock):
|
||||
Remove the dock from the parent dock area.
|
||||
"""
|
||||
# self.cleanup()
|
||||
self.parent_dock_area.remove_dock(self.name())
|
||||
self.orig_area.remove_dock(self.name())
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
|
||||
@@ -16,10 +16,14 @@ from .dock import BECDock, DockConfig
|
||||
|
||||
class DockAreaConfig(ConnectionConfig):
|
||||
docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.")
|
||||
docks_state: Optional[dict] = Field(
|
||||
None, description="The state of the docks in the dock area."
|
||||
)
|
||||
|
||||
|
||||
class BECDockArea(BECConnector, DockArea):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"panels",
|
||||
"save_state",
|
||||
"remove_dock",
|
||||
@@ -29,6 +33,7 @@ class BECDockArea(BECConnector, DockArea):
|
||||
"detach_dock",
|
||||
"attach_all",
|
||||
"get_all_rpc",
|
||||
"temp_areas",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -56,7 +61,7 @@ class BECDockArea(BECConnector, DockArea):
|
||||
painter.drawText(self.rect(), Qt.AlignCenter, "Add docks using 'add_dock' method")
|
||||
|
||||
@property
|
||||
def panels(self) -> dict:
|
||||
def panels(self) -> dict[str, BECDock]:
|
||||
"""
|
||||
Get the docks in the dock area.
|
||||
Returns:
|
||||
@@ -65,10 +70,23 @@ class BECDockArea(BECConnector, DockArea):
|
||||
return dict(self.docks)
|
||||
|
||||
@panels.setter
|
||||
def panels(self, value: dict):
|
||||
|
||||
def panels(self, value: dict[str, BECDock]):
|
||||
self.docks = WeakValueDictionary(value)
|
||||
|
||||
@property
|
||||
def temp_areas(self) -> list:
|
||||
"""
|
||||
Get the temporary areas in the dock area.
|
||||
|
||||
Returns:
|
||||
list: The temporary areas in the dock area.
|
||||
"""
|
||||
return list(map(str, self.tempAreas))
|
||||
|
||||
@temp_areas.setter
|
||||
def temp_areas(self, value: list):
|
||||
self.tempAreas = list(map(str, value))
|
||||
|
||||
def restore_state(
|
||||
self, state: dict = None, missing: Literal["ignore", "error"] = "ignore", extra="bottom"
|
||||
):
|
||||
@@ -81,7 +99,7 @@ class BECDockArea(BECConnector, DockArea):
|
||||
extra(str): Extra docks that are in the dockarea but that are not mentioned in state will be added to the bottom of the dockarea, unless otherwise specified by the extra argument.
|
||||
"""
|
||||
if state is None:
|
||||
state = self._last_state
|
||||
state = self.config.docks_state
|
||||
self.restoreState(state, missing=missing, extra=extra)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
@@ -91,8 +109,9 @@ class BECDockArea(BECConnector, DockArea):
|
||||
Returns:
|
||||
dict: The state of the dock area.
|
||||
"""
|
||||
self._last_state = self.saveState()
|
||||
return self._last_state
|
||||
last_state = self.saveState()
|
||||
self.config.docks_state = last_state
|
||||
return last_state
|
||||
|
||||
def remove_dock(self, name: str):
|
||||
"""
|
||||
@@ -102,6 +121,7 @@ class BECDockArea(BECConnector, DockArea):
|
||||
name(str): The name of the dock to remove.
|
||||
"""
|
||||
dock = self.docks.pop(name, None)
|
||||
self.config.docks.pop(name, None)
|
||||
if dock:
|
||||
dock.close()
|
||||
if len(self.docks) <= 1:
|
||||
@@ -117,6 +137,7 @@ class BECDockArea(BECConnector, DockArea):
|
||||
position: Literal["bottom", "top", "left", "right", "above", "below"] = None,
|
||||
relative_to: BECDock | None = None,
|
||||
closable: bool = False,
|
||||
floating: bool = False,
|
||||
prefix: str = "dock",
|
||||
widget: str | QWidget | None = None,
|
||||
row: int = None,
|
||||
@@ -132,6 +153,7 @@ class BECDockArea(BECConnector, DockArea):
|
||||
position(Literal["bottom", "top", "left", "right", "above", "below"]): The position of the dock.
|
||||
relative_to(BECDock): The dock to which the new dock should be added relative to.
|
||||
closable(bool): Whether the dock is closable.
|
||||
floating(bool): Whether the dock is detached after creating.
|
||||
prefix(str): The prefix for the dock name if no name is provided.
|
||||
widget(str|QWidget|None): The widget to be added to the dock. While using RPC, only BEC RPC widgets from RPCWidgetHandler are allowed.
|
||||
row(int): The row of the added widget.
|
||||
@@ -166,14 +188,14 @@ class BECDockArea(BECConnector, DockArea):
|
||||
dock.show_title_bar()
|
||||
|
||||
if widget is not None and isinstance(widget, str):
|
||||
dock.add_widget_bec(
|
||||
widget_type=widget, row=row, col=col, rowspan=rowspan, colspan=colspan
|
||||
)
|
||||
dock.add_widget(widget=widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
elif widget is not None and isinstance(widget, QWidget):
|
||||
dock.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
if self._instructions_visible:
|
||||
self._instructions_visible = False
|
||||
self.update()
|
||||
if floating:
|
||||
dock.detach()
|
||||
return dock
|
||||
|
||||
def detach_dock(self, dock_name: str) -> BECDock:
|
||||
@@ -187,7 +209,7 @@ class BECDockArea(BECConnector, DockArea):
|
||||
BECDock: The undocked dock.
|
||||
"""
|
||||
dock = self.docks[dock_name]
|
||||
self.floatDock(dock)
|
||||
dock.detach()
|
||||
return dock
|
||||
|
||||
def attach_all(self):
|
||||
|
||||
@@ -26,7 +26,7 @@ class FigureConfig(ConnectionConfig):
|
||||
theme: Literal["dark", "light"] = Field("dark", description="The theme of the figure widget.")
|
||||
num_cols: int = Field(1, description="The number of columns in the figure widget.")
|
||||
num_rows: int = Field(1, description="The number of rows in the figure widget.")
|
||||
widgets: dict[str, SubplotConfig] = Field(
|
||||
widgets: dict[str, Waveform1DConfig | ImageConfig | MotorMapConfig | SubplotConfig] = Field(
|
||||
{}, description="The list of widgets to be added to the figure widget."
|
||||
)
|
||||
|
||||
@@ -184,8 +184,9 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
"""
|
||||
self._widgets = value
|
||||
|
||||
def add_plot(
|
||||
def _init_waveform(
|
||||
self,
|
||||
waveform,
|
||||
x_name: str = None,
|
||||
y_name: str = None,
|
||||
z_name: str = None,
|
||||
@@ -198,33 +199,45 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform:
|
||||
):
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
Configure the waveform based on the provided parameters.
|
||||
|
||||
Args:
|
||||
widget_id(str): The unique identifier of the widget. If not provided, a unique ID will be generated.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
|
||||
waveform (BECWaveform): The waveform to configure.
|
||||
x (list | np.ndarray): Custom x data to plot.
|
||||
y (list | np.ndarray): Custom y data to plot.
|
||||
x_name (str): The name of the device for the x-axis.
|
||||
y_name (str): The name of the device for the y-axis.
|
||||
z_name (str): The name of the device for the z-axis.
|
||||
x_entry (str): The name of the entry for the x-axis.
|
||||
y_entry (str): The name of the entry for the y-axis.
|
||||
z_entry (str): The name of the entry for the z-axis.
|
||||
color (str): The color of the curve.
|
||||
color_map_z (str): The color map to use for the z-axis.
|
||||
label (str): The label of the curve.
|
||||
validate (bool): If True, validate the device names and entries.
|
||||
"""
|
||||
widget_id = str(uuid.uuid4())
|
||||
waveform = self.add_widget(
|
||||
widget_type="Waveform1D",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
# TODO remove repetition from .plot method
|
||||
|
||||
if x is not None and y is None:
|
||||
if isinstance(x, np.ndarray):
|
||||
if x.ndim == 1:
|
||||
y = np.arange(x.size)
|
||||
waveform.add_curve_custom(x=np.arange(x.size), y=x, color=color, label=label)
|
||||
return waveform
|
||||
if x.ndim == 2:
|
||||
waveform.add_curve_custom(x=x[:, 0], y=x[:, 1], color=color, label=label)
|
||||
return waveform
|
||||
elif isinstance(x, list):
|
||||
y = np.arange(len(x))
|
||||
waveform.add_curve_custom(x=np.arange(len(x)), y=x, color=color, label=label)
|
||||
return waveform
|
||||
else:
|
||||
raise ValueError(
|
||||
"Invalid input. Provide either device names (x_name, y_name) or custom data."
|
||||
)
|
||||
if x is not None and y is not None:
|
||||
waveform.add_curve_custom(x=x, y=y, color=color, label=label)
|
||||
return waveform
|
||||
# User wants to add scan curve -> 1D Waveform
|
||||
if x_name is not None and y_name is not None and z_name is None and x is None and y is None:
|
||||
waveform.add_curve_scan(
|
||||
@@ -262,6 +275,73 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
|
||||
return waveform
|
||||
|
||||
def add_plot(
|
||||
self,
|
||||
x: list | np.ndarray = None,
|
||||
y: list | np.ndarray = None,
|
||||
x_name: str = None,
|
||||
y_name: str = None,
|
||||
z_name: str = None,
|
||||
x_entry: str = None,
|
||||
y_entry: str = None,
|
||||
z_entry: str = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
|
||||
Args:
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
y_entry(str): The name of the entry for the y-axis.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
|
||||
"""
|
||||
widget_id = str(uuid.uuid4())
|
||||
waveform = self.add_widget(
|
||||
widget_type="Waveform1D",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
waveform = self._init_waveform(
|
||||
waveform=waveform,
|
||||
x=x,
|
||||
y=y,
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
return waveform
|
||||
|
||||
@typechecked
|
||||
def plot(
|
||||
self,
|
||||
@@ -309,70 +389,61 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
else:
|
||||
waveform = self.add_plot(**axis_kwargs)
|
||||
|
||||
if x is not None and y is None:
|
||||
if isinstance(x, np.ndarray):
|
||||
if x.ndim == 1:
|
||||
y = np.arange(x.size)
|
||||
waveform.add_curve_custom(x=np.arange(x.size), y=x, color=color, label=label)
|
||||
return waveform
|
||||
if x.ndim == 2:
|
||||
waveform.add_curve_custom(x=x[:, 0], y=x[:, 1], color=color, label=label)
|
||||
return waveform
|
||||
elif isinstance(x, list):
|
||||
y = np.arange(len(x))
|
||||
waveform.add_curve_custom(x=np.arange(len(x)), y=x, color=color, label=label)
|
||||
return waveform
|
||||
else:
|
||||
raise ValueError(
|
||||
"Invalid input. Provide either device names (x_name, y_name) or custom data."
|
||||
)
|
||||
if x is not None and y is not None:
|
||||
waveform.add_curve_custom(x=x, y=y, color=color, label=label)
|
||||
return waveform
|
||||
|
||||
# User wants to add scan curve -> 1D Waveform
|
||||
if x_name is not None and y_name is not None and z_name is None and x is None and y is None:
|
||||
waveform.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
color=color,
|
||||
color_map_z="plasma",
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
# User wants to add scan curve -> 2D Waveform Scatter
|
||||
elif (
|
||||
x_name is not None
|
||||
and y_name is not None
|
||||
and z_name is not None
|
||||
and x is None
|
||||
and y is None
|
||||
):
|
||||
waveform.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
# User wants to add custom curve
|
||||
elif (
|
||||
x is not None and y is not None and x_name is None and y_name is None and z_name is None
|
||||
):
|
||||
waveform.add_curve_custom(x=x, y=y, color=color, label=label)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Invalid input. Provide either device names (x_name, y_name) or custom data."
|
||||
)
|
||||
waveform = self._init_waveform(
|
||||
waveform=waveform,
|
||||
x=x,
|
||||
y=y,
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
# TODO remove repetition from .plot method
|
||||
return waveform
|
||||
|
||||
def _init_image(
|
||||
self,
|
||||
image,
|
||||
monitor: str = None,
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
) -> BECImageShow:
|
||||
"""
|
||||
Configure the image based on the provided parameters.
|
||||
|
||||
Args:
|
||||
image (BECImageShow): The image to configure.
|
||||
monitor (str): The name of the monitor to display.
|
||||
color_bar (Literal["simple","full"]): The type of color bar to display.
|
||||
color_map (str): The color map to use for the image.
|
||||
data (np.ndarray): Custom data to display.
|
||||
"""
|
||||
if monitor is not None and data is None:
|
||||
image.add_monitor_image(
|
||||
monitor=monitor, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is not None and monitor is None:
|
||||
image.add_custom_image(
|
||||
name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is None and monitor is None:
|
||||
# Setting appearance
|
||||
if vrange is not None:
|
||||
image.set_vrange(vmin=vrange[0], vmax=vrange[1])
|
||||
if color_map is not None:
|
||||
image.set_color_map(color_map)
|
||||
else:
|
||||
raise ValueError("Invalid input. Provide either monitor name or custom data.")
|
||||
return image
|
||||
|
||||
def image(
|
||||
self,
|
||||
monitor: str = None,
|
||||
@@ -405,23 +476,14 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
else:
|
||||
image = self.add_image(color_bar=color_bar, **axis_kwargs)
|
||||
|
||||
# Setting data #TODO check logic if monitor or data are already created
|
||||
if monitor is not None and data is None:
|
||||
image.add_monitor_image(
|
||||
monitor=monitor, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is not None and monitor is None:
|
||||
image.add_custom_image(
|
||||
name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is None and monitor is None:
|
||||
# Setting appearance
|
||||
if vrange is not None:
|
||||
image.set_vrange(vmin=vrange[0], vmax=vrange[1])
|
||||
if color_map is not None:
|
||||
image.set_color_map(color_map)
|
||||
else:
|
||||
raise ValueError("Invalid input. Provide either monitor name or custom data.")
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
color_bar=color_bar,
|
||||
color_map=color_map,
|
||||
data=data,
|
||||
vrange=vrange,
|
||||
)
|
||||
return image
|
||||
|
||||
def add_image(
|
||||
@@ -472,22 +534,14 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
# TODO remove repetition from .image method
|
||||
if monitor is not None and data is None:
|
||||
image.add_monitor_image(
|
||||
monitor=monitor, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is not None and monitor is None:
|
||||
image.add_custom_image(
|
||||
name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is None and monitor is None:
|
||||
# Setting appearance
|
||||
if vrange is not None:
|
||||
image.set_vrange(vmin=vrange[0], vmax=vrange[1])
|
||||
if color_map is not None:
|
||||
image.set_color_map(color_map)
|
||||
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
color_bar=color_bar,
|
||||
color_map=color_map,
|
||||
data=data,
|
||||
vrange=vrange,
|
||||
)
|
||||
return image
|
||||
|
||||
def motor_map(self, motor_x: str = None, motor_y: str = None, **axis_kwargs) -> BECMotorMap:
|
||||
|
||||
@@ -53,7 +53,6 @@ class BECImageShow(BECPlotBase):
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"lock_aspect_ratio",
|
||||
"plot",
|
||||
"remove",
|
||||
"images",
|
||||
]
|
||||
|
||||
@@ -19,6 +19,7 @@ class ProcessingConfig(BaseModel):
|
||||
rotation: Optional[int] = Field(
|
||||
None, description="The rotation angle of the monitor data before displaying."
|
||||
)
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
|
||||
@@ -317,7 +317,7 @@ class BECMotorMap(BECPlotBase):
|
||||
Returns:
|
||||
float: Motor initial position.
|
||||
"""
|
||||
init_position = round(self.dev[name].read()[entry]["value"], precision)
|
||||
init_position = round(float(self.dev[name].read()[entry]["value"]), precision)
|
||||
return init_position
|
||||
|
||||
def _validate_signal_entries(
|
||||
@@ -403,7 +403,7 @@ class BECMotorMap(BECPlotBase):
|
||||
# Update plot title
|
||||
precision = self.config.precision
|
||||
self.set_title(
|
||||
f"Motor position: ({round(current_x,precision)}, {round(current_y,precision)})"
|
||||
f"Motor position: ({round(float(current_x),precision)}, {round(float(current_y),precision)})"
|
||||
)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
|
||||
@@ -20,6 +20,7 @@ class AxisConfig(BaseModel):
|
||||
y_lim: Optional[tuple] = Field(None, description="The limits of the y-axis.")
|
||||
x_grid: bool = Field(False, description="Show grid on the x-axis.")
|
||||
y_grid: bool = Field(False, description="Show grid on the y-axis.")
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class SubplotConfig(ConnectionConfig):
|
||||
@@ -48,7 +49,6 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"lock_aspect_ratio",
|
||||
"plot",
|
||||
"remove",
|
||||
]
|
||||
|
||||
@@ -237,19 +237,6 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
"""
|
||||
self.plot_item.setAspectLocked(lock)
|
||||
|
||||
def plot(self, data_x: list | np.ndarray, data_y: list | np.ndarray, **kwargs):
|
||||
"""
|
||||
Plot custom data on the plot widget. These data are not saved in config.
|
||||
|
||||
Args:
|
||||
data_x(list|np.ndarray): x-axis data
|
||||
data_y(list|np.ndarray): y-axis data
|
||||
**kwargs: Keyword arguments for the plot.
|
||||
"""
|
||||
# TODO very basic so far, add more options
|
||||
# TODO decide name of the method
|
||||
self.plot_item.plot(data_x, data_y, **kwargs)
|
||||
|
||||
def remove(self):
|
||||
"""Remove the plot widget from the figure."""
|
||||
if self.figure is not None:
|
||||
|
||||
@@ -35,8 +35,7 @@ class BECWaveform(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"add_curve_scan",
|
||||
"add_curve_custom",
|
||||
"plot",
|
||||
"remove_curve",
|
||||
"scan_history",
|
||||
"curves",
|
||||
@@ -54,7 +53,6 @@ class BECWaveform(BECPlotBase):
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"lock_aspect_ratio",
|
||||
"plot",
|
||||
"remove",
|
||||
]
|
||||
scan_signal_update = pyqtSignal()
|
||||
@@ -200,6 +198,57 @@ class BECWaveform(BECPlotBase):
|
||||
else:
|
||||
raise ValueError("Identifier must be either an integer (index) or a string (curve_id).")
|
||||
|
||||
def plot(
|
||||
self,
|
||||
x: list | np.ndarray | None = None,
|
||||
y: list | np.ndarray | None = None,
|
||||
x_name: str | None = None,
|
||||
y_name: str | None = None,
|
||||
z_name: str | None = None,
|
||||
x_entry: str | None = None,
|
||||
y_entry: str | None = None,
|
||||
z_entry: str | None = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "plasma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Plot a curve to the plot widget.
|
||||
Args:
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
y_entry(str): The name of the entry for the y-axis.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
if x is not None and y is not None:
|
||||
return self.add_curve_custom(x=x, y=y, label=label, color=color)
|
||||
else:
|
||||
return self.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate_bec=validate,
|
||||
)
|
||||
|
||||
def add_curve_custom(
|
||||
self,
|
||||
x: list | np.ndarray,
|
||||
@@ -252,33 +301,6 @@ class BECWaveform(BECPlotBase):
|
||||
)
|
||||
return curve
|
||||
|
||||
def _add_curve_object(
|
||||
self,
|
||||
name: str,
|
||||
source: str,
|
||||
config: CurveConfig,
|
||||
data: tuple[list | np.ndarray, list | np.ndarray] = None,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Add a curve object to the plot widget.
|
||||
|
||||
Args:
|
||||
name(str): ID of the curve.
|
||||
source(str): Source of the curve.
|
||||
config(CurveConfig): Configuration of the curve.
|
||||
data(tuple[list|np.ndarray,list|np.ndarray], optional): Data (x,y) to be plotted. Defaults to None.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
curve = BECCurve(config=config, name=name, parent_item=self.plot_item)
|
||||
self._curves_data[source][name] = curve
|
||||
self.plot_item.addItem(curve)
|
||||
self.config.curves[name] = curve.config
|
||||
if data is not None:
|
||||
curve.setData(data[0], data[1])
|
||||
return curve
|
||||
|
||||
def add_curve_scan(
|
||||
self,
|
||||
x_name: str,
|
||||
@@ -341,7 +363,7 @@ class BECWaveform(BECPlotBase):
|
||||
parent_id=self.gui_id,
|
||||
label=label,
|
||||
color=color,
|
||||
color_map=color_map_z,
|
||||
color_map_z=color_map_z,
|
||||
source=curve_source,
|
||||
signals=Signal(
|
||||
source=curve_source,
|
||||
@@ -354,6 +376,33 @@ class BECWaveform(BECPlotBase):
|
||||
curve = self._add_curve_object(name=label, source=curve_source, config=curve_config)
|
||||
return curve
|
||||
|
||||
def _add_curve_object(
|
||||
self,
|
||||
name: str,
|
||||
source: str,
|
||||
config: CurveConfig,
|
||||
data: tuple[list | np.ndarray, list | np.ndarray] = None,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Add a curve object to the plot widget.
|
||||
|
||||
Args:
|
||||
name(str): ID of the curve.
|
||||
source(str): Source of the curve.
|
||||
config(CurveConfig): Configuration of the curve.
|
||||
data(tuple[list|np.ndarray,list|np.ndarray], optional): Data (x,y) to be plotted. Defaults to None.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
curve = BECCurve(config=config, name=name, parent_item=self)
|
||||
self._curves_data[source][name] = curve
|
||||
self.plot_item.addItem(curve)
|
||||
self.config.curves[name] = curve.config
|
||||
if data is not None:
|
||||
curve.setData(data[0], data[1])
|
||||
return curve
|
||||
|
||||
def _validate_signal_entries(
|
||||
self,
|
||||
x_name: str,
|
||||
@@ -514,7 +563,7 @@ class BECWaveform(BECPlotBase):
|
||||
if curve.config.signals.z:
|
||||
data_z = data[z_name][z_entry].val
|
||||
color_z = self._make_z_gradient(
|
||||
data_z, curve.config.colormap
|
||||
data_z, curve.config.color_map_z
|
||||
) # TODO decide how to implement custom gradient
|
||||
except TypeError:
|
||||
continue
|
||||
@@ -577,7 +626,6 @@ class BECWaveform(BECPlotBase):
|
||||
Returns:
|
||||
dict | pd.DataFrame: Data of all curves in the specified format.
|
||||
"""
|
||||
|
||||
data = {}
|
||||
try:
|
||||
import pandas as pd
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal, Optional
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtCore
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.figure.plots.waveform import BECWaveform1D
|
||||
|
||||
|
||||
class SignalData(BaseModel):
|
||||
@@ -17,6 +21,7 @@ class SignalData(BaseModel):
|
||||
unit: Optional[str] = None # todo implement later
|
||||
modifier: Optional[str] = None # todo implement later
|
||||
limits: Optional[list[float]] = None # todo implement later
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class Signal(BaseModel):
|
||||
@@ -26,14 +31,17 @@ class Signal(BaseModel):
|
||||
x: SignalData # TODO maybe add metadata for config gui later
|
||||
y: SignalData
|
||||
z: Optional[SignalData] = None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class CurveConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent plot of the curve.")
|
||||
label: Optional[str] = Field(None, description="The label of the curve.")
|
||||
color: Optional[Any] = Field(None, description="The color of the curve.")
|
||||
color: Optional[str | tuple] = Field(None, description="The color of the curve.")
|
||||
symbol: Optional[str] = Field("o", description="The symbol of the curve.")
|
||||
symbol_color: Optional[str] = Field(None, description="The color of the symbol of the curve.")
|
||||
symbol_color: Optional[str | tuple] = Field(
|
||||
None, description="The color of the symbol of the curve."
|
||||
)
|
||||
symbol_size: Optional[int] = Field(5, description="The size of the symbol of the curve.")
|
||||
pen_width: Optional[int] = Field(2, description="The width of the pen of the curve.")
|
||||
pen_style: Optional[Literal["solid", "dash", "dot", "dashdot"]] = Field(
|
||||
@@ -41,7 +49,15 @@ class CurveConfig(ConnectionConfig):
|
||||
)
|
||||
source: Optional[str] = Field(None, description="The source of the curve.")
|
||||
signals: Optional[Signal] = Field(None, description="The signal of the curve.")
|
||||
colormap: Optional[str] = Field("plasma", description="The colormap of the curves z gradient.")
|
||||
color_map_z: Optional[str] = Field(
|
||||
"plasma", description="The colormap of the curves z gradient.", validate_default=True
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
_validate_color_map_z = field_validator("color_map_z")(Colors.validate_color_map)
|
||||
_validate_color = field_validator("color")(Colors.validate_color)
|
||||
_validate_symbol_color = field_validator("symbol_color")(Colors.validate_color)
|
||||
|
||||
|
||||
class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
@@ -52,7 +68,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
"set",
|
||||
"set_data",
|
||||
"set_color",
|
||||
"set_colormap",
|
||||
"set_color_map_z",
|
||||
"set_symbol",
|
||||
"set_symbol_color",
|
||||
"set_symbol_size",
|
||||
@@ -66,7 +82,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
name: Optional[str] = None,
|
||||
config: Optional[CurveConfig] = None,
|
||||
gui_id: Optional[str] = None,
|
||||
parent_item: Optional[pg.PlotItem] = None,
|
||||
parent_item: Optional[BECWaveform1D] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
@@ -128,7 +144,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"color": self.set_color,
|
||||
"colormap": self.set_colormap,
|
||||
"color_map_z": self.set_color_map_z,
|
||||
"symbol": self.set_symbol,
|
||||
"symbol_color": self.set_symbol_color,
|
||||
"symbol_size": self.set_symbol_size,
|
||||
@@ -203,14 +219,16 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
self.config.pen_style = pen_style
|
||||
self.apply_config()
|
||||
|
||||
def set_colormap(self, colormap: str):
|
||||
def set_color_map_z(self, colormap: str):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
self.config.colormap = colormap
|
||||
self.config.color_map_z = colormap
|
||||
self.apply_config()
|
||||
self.parent_item.scan_history(-1)
|
||||
|
||||
def get_data(self) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
|
||||
0
bec_widgets/widgets/jupyter_console/__init__.py
Normal file
72
bec_widgets/widgets/jupyter_console/jupyter_console.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from bec_ipython_client.main import BECIPythonClient
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
from qtconsole.manager import QtKernelManager
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
from qtpy.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
|
||||
class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
|
||||
def __init__(self, inprocess: bool = False):
|
||||
super().__init__()
|
||||
|
||||
self.inprocess = None
|
||||
|
||||
self.kernel_manager, self.kernel_client = self._init_kernel(inprocess=inprocess)
|
||||
self.set_default_style("linux")
|
||||
self._init_bec()
|
||||
|
||||
def _init_kernel(self, inprocess: bool = False, kernel_name: str = "python3"):
|
||||
self.inprocess = inprocess
|
||||
if inprocess is True:
|
||||
print("starting inprocess kernel")
|
||||
kernel_manager = QtInProcessKernelManager()
|
||||
else:
|
||||
kernel_manager = QtKernelManager(kernel_name=kernel_name)
|
||||
kernel_manager.start_kernel()
|
||||
kernel_client = kernel_manager.client()
|
||||
kernel_client.start_channels()
|
||||
return kernel_manager, kernel_client
|
||||
|
||||
def _init_bec(self):
|
||||
if self.inprocess is True:
|
||||
self._init_bec_inprocess()
|
||||
else:
|
||||
self._init_bec_kernel()
|
||||
|
||||
def _init_bec_inprocess(self):
|
||||
self.client = BECIPythonClient()
|
||||
self.client.start()
|
||||
|
||||
self.kernel_manager.kernel.shell.push(
|
||||
{
|
||||
"bec": self.client,
|
||||
"dev": self.client.device_manager.devices,
|
||||
"scans": self.client.scans,
|
||||
}
|
||||
)
|
||||
|
||||
def _init_bec_kernel(self):
|
||||
self.execute(
|
||||
"""
|
||||
from bec_ipython_client.main import BECIPythonClient
|
||||
bec = BECIPythonClient()
|
||||
bec.start()
|
||||
dev = bec.device_manager.devices if bec else None
|
||||
scans = bec.scans if bec else None
|
||||
"""
|
||||
)
|
||||
|
||||
def shutdown_kernel(self):
|
||||
self.kernel_client.stop_channels()
|
||||
self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
win = QMainWindow()
|
||||
win.setCentralWidget(BECJupyterConsole(True))
|
||||
win.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@@ -13,20 +13,21 @@ from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
class RingConnections(BaseModel):
|
||||
slot: Literal["on_scan_progress", "on_device_readback"] = None
|
||||
endpoint: EndpointInfo | str = None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
@field_validator("endpoint")
|
||||
def validate_endpoint(cls, v, values):
|
||||
slot = values.data["slot"]
|
||||
endpoint = v.endpoint if isinstance(v, EndpointInfo) else v
|
||||
v = v.endpoint if isinstance(v, EndpointInfo) else v
|
||||
if slot == "on_scan_progress":
|
||||
if endpoint != "scans/scan_progress":
|
||||
if v != "scans/scan_progress":
|
||||
raise PydanticCustomError(
|
||||
"unsupported endpoint",
|
||||
"For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'.",
|
||||
{"wrong_value": v},
|
||||
)
|
||||
elif slot == "on_device_readback":
|
||||
if not endpoint.startswith("internal/devices/readback/"):
|
||||
if not v.startswith("internal/devices/readback/"):
|
||||
raise PydanticCustomError(
|
||||
"unsupported endpoint",
|
||||
"For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'.",
|
||||
@@ -36,6 +37,7 @@ class RingConnections(BaseModel):
|
||||
|
||||
|
||||
class RingConfig(ConnectionConfig):
|
||||
value: int | float | None = Field(0, description="Value for the progress bars.")
|
||||
direction: int | None = Field(
|
||||
-1, description="Direction of the progress bars. -1 for clockwise, 1 for counter-clockwise."
|
||||
)
|
||||
@@ -48,14 +50,14 @@ class RingConfig(ConnectionConfig):
|
||||
description="Background color for the progress bars. Can be tuple (R, G, B, A) or string HEX Code.",
|
||||
)
|
||||
index: int | None = Field(0, description="Index of the progress bar. 0 is outer ring.")
|
||||
line_width: int | None = Field(5, description="Line widths for the progress bars.")
|
||||
line_width: int | None = Field(10, description="Line widths for the progress bars.")
|
||||
start_position: int | None = Field(
|
||||
90,
|
||||
description="Start position for the progress bars in degrees. Default is 90 degrees - corespons to "
|
||||
"the top of the ring.",
|
||||
)
|
||||
min_value: int | None = Field(0, description="Minimum value for the progress bars.")
|
||||
max_value: int | None = Field(100, description="Maximum value for the progress bars.")
|
||||
min_value: int | float | None = Field(0, description="Minimum value for the progress bars.")
|
||||
max_value: int | float | None = Field(100, description="Maximum value for the progress bars.")
|
||||
precision: int | None = Field(3, description="Precision for the progress bars.")
|
||||
update_behaviour: Literal["manual", "auto"] | None = Field(
|
||||
"auto", description="Update behaviour for the progress bars."
|
||||
@@ -76,7 +78,7 @@ class Ring(BECConnector):
|
||||
"set_line_width",
|
||||
"set_min_max_values",
|
||||
"set_start_angle",
|
||||
"set_connections",
|
||||
"set_update",
|
||||
"reset_connection",
|
||||
]
|
||||
|
||||
@@ -102,7 +104,6 @@ class Ring(BECConnector):
|
||||
self.background_color = None
|
||||
self.start_position = None
|
||||
self.config = config
|
||||
self.value = 0
|
||||
self.RID = None
|
||||
self._init_config_params()
|
||||
|
||||
@@ -114,31 +115,75 @@ class Ring(BECConnector):
|
||||
self.set_connections(self.config.connections.slot, self.config.connections.endpoint)
|
||||
|
||||
def set_value(self, value: int | float):
|
||||
self.value = round(
|
||||
max(self.config.min_value, min(self.config.max_value, value)), self.config.precision
|
||||
"""
|
||||
Set the value for the ring widget
|
||||
|
||||
Args:
|
||||
value(int | float): Value for the ring widget
|
||||
"""
|
||||
self.config.value = round(
|
||||
float(max(self.config.min_value, min(self.config.max_value, value))),
|
||||
self.config.precision,
|
||||
)
|
||||
|
||||
def set_color(self, color: str | tuple):
|
||||
"""
|
||||
Set the color for the ring widget
|
||||
|
||||
Args:
|
||||
color(str | tuple): Color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
self.config.color = color
|
||||
self.color = self.convert_color(color)
|
||||
|
||||
def set_background(self, color: str | tuple):
|
||||
"""
|
||||
Set the background color for the ring widget
|
||||
|
||||
Args:
|
||||
color(str | tuple): Background color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
self.config.background_color = color
|
||||
self.color = self.convert_color(color)
|
||||
|
||||
def set_line_width(self, width: int):
|
||||
"""
|
||||
Set the line width for the ring widget
|
||||
|
||||
Args:
|
||||
width(int): Line width for the ring widget
|
||||
"""
|
||||
self.config.line_width = width
|
||||
|
||||
def set_min_max_values(self, min_value: int, max_value: int):
|
||||
def set_min_max_values(self, min_value: int | float, max_value: int | float):
|
||||
"""
|
||||
Set the min and max values for the ring widget.
|
||||
|
||||
Args:
|
||||
min_value(int | float): Minimum value for the ring widget
|
||||
max_value(int | float): Maximum value for the ring widget
|
||||
"""
|
||||
self.config.min_value = min_value
|
||||
self.config.max_value = max_value
|
||||
|
||||
def set_start_angle(self, start_angle: int):
|
||||
"""
|
||||
Set the start angle for the ring widget
|
||||
|
||||
Args:
|
||||
start_angle(int): Start angle for the ring widget in degrees
|
||||
"""
|
||||
self.config.start_position = start_angle
|
||||
self.start_position = start_angle * 16
|
||||
|
||||
@staticmethod
|
||||
def convert_color(color):
|
||||
"""
|
||||
Convert the color to QColor
|
||||
|
||||
Args:
|
||||
color(str | tuple): Color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
converted_color = None
|
||||
if isinstance(color, str):
|
||||
converted_color = QtGui.QColor(color)
|
||||
@@ -146,7 +191,39 @@ class Ring(BECConnector):
|
||||
converted_color = QtGui.QColor(*color)
|
||||
return converted_color
|
||||
|
||||
def set_update(self, mode: Literal["manual", "scan", "device"], device: str = None):
|
||||
"""
|
||||
Set the update mode for the ring widget.
|
||||
Modes:
|
||||
- "manual": Manual update mode, the value is set by the user.
|
||||
- "scan": Update mode for the scan progress. The value is updated by the current scan progress.
|
||||
- "device": Update mode for the device readback. The value is updated by the device readback. Take into account that user has to set the device name and limits.
|
||||
|
||||
Args:
|
||||
mode(str): Update mode for the ring widget. Can be "manual", "scan" or "device"
|
||||
device(str): Device name for the device readback mode, only used when mode is "device"
|
||||
"""
|
||||
if mode == "manual":
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
getattr(self, self.config.connections.slot), self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections.slot = None
|
||||
self.config.connections.endpoint = None
|
||||
elif mode == "scan":
|
||||
self.set_connections("on_scan_progress", "scans/scan_progress")
|
||||
elif mode == "device":
|
||||
self.set_connections("on_device_readback", f"internal/devices/readback/{device}")
|
||||
|
||||
self.parent_progress_widget.enable_auto_updates(False)
|
||||
|
||||
def set_connections(self, slot: str, endpoint: str | EndpointInfo):
|
||||
"""
|
||||
Set the connections for the ring widget
|
||||
|
||||
Args:
|
||||
slot(str): Slot for the ring widget update. Can be "on_scan_progress" or "on_device_readback".
|
||||
endpoint(str | EndpointInfo): Endpoint for the ring widget update. Endpoint has to match the slot type.
|
||||
"""
|
||||
if self.config.connections.endpoint == endpoint and self.config.connections.slot == slot:
|
||||
return
|
||||
else:
|
||||
@@ -157,12 +234,22 @@ class Ring(BECConnector):
|
||||
self.bec_dispatcher.connect_slot(getattr(self, slot), endpoint)
|
||||
|
||||
def reset_connection(self):
|
||||
"""
|
||||
Reset the connections for the ring widget. Disconnect the current slot and endpoint.
|
||||
"""
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.config.connections.slot, self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections = RingConnections()
|
||||
|
||||
def on_scan_progress(self, msg, meta):
|
||||
"""
|
||||
Update the ring widget with the scan progress.
|
||||
|
||||
Args:
|
||||
msg(dict): Message with the scan progress
|
||||
meta(dict): Metadata for the message
|
||||
"""
|
||||
current_RID = meta.get("RID", None)
|
||||
if current_RID != self.RID:
|
||||
self.set_min_max_values(0, msg.get("max_value", 100))
|
||||
@@ -170,6 +257,13 @@ class Ring(BECConnector):
|
||||
self.parent_progress_widget.update()
|
||||
|
||||
def on_device_readback(self, msg, meta):
|
||||
"""
|
||||
Update the ring widget with the device readback.
|
||||
|
||||
Args:
|
||||
msg(dict): Message with the device readback
|
||||
meta(dict): Metadata for the message
|
||||
"""
|
||||
if isinstance(self.config.connections.endpoint, EndpointInfo):
|
||||
endpoint = self.config.connections.endpoint.endpoint
|
||||
else:
|
||||
|
||||
@@ -15,7 +15,9 @@ from bec_widgets.widgets.spiral_progress_bar.ring import Ring, RingConfig
|
||||
|
||||
|
||||
class SpiralProgressBarConfig(ConnectionConfig):
|
||||
color_map: str | None = Field("magma", description="Color scheme for the progress bars.")
|
||||
color_map: Optional[str] = Field(
|
||||
"magma", description="Color scheme for the progress bars.", validate_default=True
|
||||
)
|
||||
min_number_of_bars: int | None = Field(
|
||||
1, description="Minimum number of progress bars to display."
|
||||
)
|
||||
@@ -23,7 +25,7 @@ class SpiralProgressBarConfig(ConnectionConfig):
|
||||
10, description="Maximum number of progress bars to display."
|
||||
)
|
||||
num_bars: int | None = Field(1, description="Number of progress bars to display.")
|
||||
gap: int | None = Field(10, description="Gap between progress bars.")
|
||||
gap: int | None = Field(20, description="Gap between progress bars.")
|
||||
auto_updates: bool | None = Field(
|
||||
True, description="Enable or disable updates based on scan queue status."
|
||||
)
|
||||
@@ -59,16 +61,7 @@ class SpiralProgressBarConfig(ConnectionConfig):
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("color_map")
|
||||
def validate_color_map(cls, v, values):
|
||||
if v is not None and v != "":
|
||||
if v not in pg.colormap.listMaps():
|
||||
raise PydanticCustomError(
|
||||
"unsupported colormap",
|
||||
f"Colormap '{v}' not found in the current installation of pyqtgraph",
|
||||
{"wrong_value": v},
|
||||
)
|
||||
return v
|
||||
_validate_colormap = field_validator("color_map")(Colors.validate_color_map)
|
||||
|
||||
|
||||
class SpiralProgressBar(BECConnector, QWidget):
|
||||
@@ -115,11 +108,10 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
self.RID = None
|
||||
self.values = None
|
||||
|
||||
# For updating bar behaviour
|
||||
self._auto_updates = True
|
||||
self._rings = []
|
||||
self._rings = None
|
||||
|
||||
if num_bars is not None:
|
||||
self.config.num_bars = max(
|
||||
@@ -187,13 +179,22 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
Ring: Ring object.
|
||||
"""
|
||||
if self.config.num_bars < self.config.max_number_of_bars:
|
||||
ring = Ring(parent_progress_widget=self, **kwargs)
|
||||
ring.config.index = self.config.num_bars
|
||||
ring_index = self.config.num_bars
|
||||
ring_config = RingConfig(
|
||||
widget_class="Ring",
|
||||
index=ring_index,
|
||||
start_positions=90 * 16,
|
||||
directions=-1,
|
||||
**kwargs,
|
||||
)
|
||||
ring = Ring(parent_progress_widget=self, config=ring_config)
|
||||
self.config.num_bars += 1
|
||||
self._rings.append(ring)
|
||||
self.config.rings.append(ring.config)
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
base_line_width = self._rings[ring.config.index].config.line_width
|
||||
self.set_line_widths(base_line_width, ring.config.index)
|
||||
self.update()
|
||||
return ring
|
||||
|
||||
@@ -212,6 +213,7 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
self._reindex_rings()
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
del ring
|
||||
self.update()
|
||||
|
||||
def _reindex_rings(self):
|
||||
@@ -270,9 +272,30 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
num_bars = max(
|
||||
self.config.min_number_of_bars, min(num_bars, self.config.max_number_of_bars)
|
||||
)
|
||||
if num_bars != self.config.num_bars:
|
||||
self.config.num_bars = num_bars
|
||||
self.initialize_bars()
|
||||
current_num_bars = self.config.num_bars
|
||||
|
||||
if num_bars > current_num_bars:
|
||||
for i in range(current_num_bars, num_bars):
|
||||
new_ring_config = RingConfig(
|
||||
widget_class="Ring", index=i, start_positions=90 * 16, directions=-1
|
||||
)
|
||||
self.config.rings.append(new_ring_config)
|
||||
new_ring = Ring(parent_progress_widget=self, config=new_ring_config)
|
||||
self._rings.append(new_ring)
|
||||
|
||||
elif num_bars < current_num_bars:
|
||||
for i in range(current_num_bars - 1, num_bars - 1, -1):
|
||||
self.remove_ring(i)
|
||||
|
||||
self.config.num_bars = num_bars
|
||||
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
|
||||
base_line_width = self._rings[0].config.line_width
|
||||
self.set_line_widths(base_line_width)
|
||||
|
||||
self.update()
|
||||
|
||||
def set_value(self, values: int | list, ring_index: int = None):
|
||||
"""
|
||||
@@ -338,7 +361,6 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
colors = self._adjust_list_to_bars(colors)
|
||||
for ring, color in zip(self._rings, colors):
|
||||
ring.set_color(color)
|
||||
self.config.color_map = None
|
||||
self.update()
|
||||
|
||||
def set_line_widths(self, widths: int | list[int], bar_index: int = None):
|
||||
@@ -434,6 +456,13 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_scan_queue_status(self, msg, meta):
|
||||
"""
|
||||
Slot to handle scan queue status messages. Decides what update to perform based on the scan queue status.
|
||||
|
||||
Args:
|
||||
msg(dict): Message from the BEC.
|
||||
meta(dict): Metadata from the BEC.
|
||||
"""
|
||||
primary_queue = msg.get("queue").get("primary")
|
||||
info = primary_queue.get("info", None)
|
||||
|
||||
@@ -444,8 +473,6 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
if report_instructions:
|
||||
instruction_type = list(report_instructions[0].keys())[0]
|
||||
if instruction_type == "scan_progress":
|
||||
if self.config.num_bars != 1:
|
||||
self.set_number_of_bars(1)
|
||||
self._hook_scan_progress(ring_index=0)
|
||||
elif instruction_type == "readback":
|
||||
devices = report_instructions[0].get("readback").get("devices")
|
||||
@@ -462,6 +489,12 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
# print("hook device_progress")
|
||||
|
||||
def _hook_scan_progress(self, ring_index: int = None):
|
||||
"""
|
||||
Hook the scan progress to the progress bars.
|
||||
|
||||
Args:
|
||||
ring_index(int): Index of the progress bar to hook the scan progress to.
|
||||
"""
|
||||
if ring_index is not None:
|
||||
ring = self._find_ring_by_index(ring_index)
|
||||
else:
|
||||
@@ -473,6 +506,15 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
ring.set_connections("on_scan_progress", MessageEndpoints.scan_progress())
|
||||
|
||||
def _hook_readback(self, bar_index: int, device: str, min: float | int, max: float | int):
|
||||
"""
|
||||
Hook the readback values to the progress bars.
|
||||
|
||||
Args:
|
||||
bar_index(int): Index of the progress bar to hook the readback values to.
|
||||
device(str): Device to readback values from.
|
||||
min(float|int): Minimum value for the progress bar.
|
||||
max(float|int): Maximum value for the progress bar.
|
||||
"""
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
ring.set_min_max_values(min, max)
|
||||
endpoint = MessageEndpoints.device_readback(device)
|
||||
@@ -544,7 +586,7 @@ class SpiralProgressBar(BECConnector, QWidget):
|
||||
pen = QtGui.QPen(ring.color, ring.config.line_width, QtCore.Qt.SolidLine)
|
||||
pen.setCapStyle(QtCore.Qt.RoundCap)
|
||||
painter.setPen(pen)
|
||||
proportion = (ring.value - ring.config.min_value) / (
|
||||
proportion = (ring.config.value - ring.config.min_value) / (
|
||||
(ring.config.max_value - ring.config.min_value) + 1e-3
|
||||
)
|
||||
angle = int(proportion * 360 * 16 * ring.config.direction)
|
||||
|
||||
0
bec_widgets/widgets/website/__init__.py
Normal file
65
bec_widgets/widgets/website/website.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from qtpy.QtCore import QUrl
|
||||
from qtpy.QtWebEngineWidgets import QWebEngineView
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
|
||||
class WebsiteWidget(BECConnector, QWebEngineView):
|
||||
"""
|
||||
A simple widget to display a website
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["set_url", "get_url", "reload", "back", "forward"]
|
||||
|
||||
def __init__(self, url: str = None, parent=None, config=None, client=None, gui_id=None):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QWebEngineView.__init__(self, parent=parent)
|
||||
self.set_url(url)
|
||||
|
||||
def set_url(self, url: str) -> None:
|
||||
"""
|
||||
Set the url of the website widget
|
||||
|
||||
Args:
|
||||
url (str): The url to set
|
||||
"""
|
||||
if not url:
|
||||
return
|
||||
self.setUrl(QUrl(url))
|
||||
|
||||
def get_url(self) -> str:
|
||||
"""
|
||||
Get the current url of the website widget
|
||||
|
||||
Returns:
|
||||
str: The current url
|
||||
"""
|
||||
return self.url().toString()
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
Reload the website
|
||||
"""
|
||||
QWebEngineView.reload(self)
|
||||
|
||||
def back(self):
|
||||
"""
|
||||
Go back in the history
|
||||
"""
|
||||
QWebEngineView.back(self)
|
||||
|
||||
def forward(self):
|
||||
"""
|
||||
Go forward in the history
|
||||
"""
|
||||
QWebEngineView.forward(self)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
mainWin = WebsiteWidget("https://scilog.psi.ch")
|
||||
mainWin.show()
|
||||
sys.exit(app.exec())
|
||||
170
docs/_static/custom.css
vendored
Normal file
@@ -1,4 +1,5 @@
|
||||
## API Reference
|
||||
(api_reference)=
|
||||
# API Reference
|
||||
|
||||
```{eval-rst}
|
||||
.. autosummary::
|
||||
@@ -7,4 +8,5 @@
|
||||
:recursive:
|
||||
|
||||
bec_widgets
|
||||
|
||||
```
|
||||
1
docs/assets/apps_48dp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z"/></svg>
|
||||
|
After Width: | Height: | Size: 292 B |
1
docs/assets/display_settings_48dp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="48px" viewBox="0 0 20 20" width="48px" fill="#000000"><g><rect fill="none" height="20" width="20" x="0"/></g><g><g><path d="M16.5,3h-13C2.67,3,2,3.67,2,4.5v9C2,14.33,2.67,15,3.5,15H7v2h6v-2h3.5c0.83,0,1.5-0.67,1.5-1.5v-9 C18,3.67,17.33,3,16.5,3z M16.5,13.5h-13v-9h13V13.5z"/><rect height="1" width="7" x="5" y="7"/><polygon points="14,8 15,8 15,7 14,7 14,6 13,6 13,9 14,9"/><rect height="1" width="7" x="8" y="10"/><polygon points="6,12 7,12 7,9 6,9 6,10 5,10 5,11 6,11"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 568 B |
97
docs/assets/index_api.svg
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="99.058548mm"
|
||||
height="89.967583mm"
|
||||
viewBox="0 0 99.058554 89.967582"
|
||||
version="1.1"
|
||||
id="svg1040"
|
||||
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
|
||||
sodipodi:docname="index_api.svg">
|
||||
<defs
|
||||
id="defs1034" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="533.74914"
|
||||
inkscape:cy="10.90433"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="930"
|
||||
inkscape:window-height="472"
|
||||
inkscape:window-x="2349"
|
||||
inkscape:window-y="267"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata1037">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(195.19933,-1.0492759)">
|
||||
<g
|
||||
id="g1008"
|
||||
transform="matrix(1.094977,0,0,1.094977,-521.5523,-198.34055)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path899"
|
||||
d="M 324.96812,187.09499 H 303.0455 v 72.1639 h 22.67969"
|
||||
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path899-3"
|
||||
d="m 361.58921,187.09499 h 21.92262 v 72.1639 h -22.67969"
|
||||
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<g
|
||||
transform="translate(415.87139,46.162126)"
|
||||
id="g944">
|
||||
<circle
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path918"
|
||||
cx="-84.40152"
|
||||
cy="189.84375"
|
||||
r="2.2293637" />
|
||||
<circle
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path918-5"
|
||||
cx="-72.949402"
|
||||
cy="189.84375"
|
||||
r="2.2293637" />
|
||||
<circle
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path918-6"
|
||||
cx="-61.497284"
|
||||
cy="189.84375"
|
||||
r="2.2293637" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
76
docs/assets/index_contribute.svg
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="89.624855mm"
|
||||
height="89.96759mm"
|
||||
viewBox="0 0 89.62486 89.96759"
|
||||
version="1.1"
|
||||
id="svg1040"
|
||||
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
|
||||
sodipodi:docname="index_contribute.svg">
|
||||
<defs
|
||||
id="defs1034" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="683.11893"
|
||||
inkscape:cy="-59.078181"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="930"
|
||||
inkscape:window-height="472"
|
||||
inkscape:window-x="2349"
|
||||
inkscape:window-y="267"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata1037">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(234.72009,17.466935)">
|
||||
<g
|
||||
id="g875"
|
||||
transform="matrix(0.99300176,0,0,0.99300176,-133.24106,-172.58804)">
|
||||
<path
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path869"
|
||||
d="m -97.139881,161.26069 47.247024,40.25446 -47.247024,40.25446"
|
||||
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path871"
|
||||
d="m -49.514879,241.81547 h 32.505951"
|
||||
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
66
docs/assets/index_getting_started.svg
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="101.09389mm"
|
||||
height="89.96759mm"
|
||||
viewBox="0 0 101.09389 89.96759"
|
||||
version="1.1"
|
||||
id="svg1040"
|
||||
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
|
||||
sodipodi:docname="index_getting_started.svg">
|
||||
<defs
|
||||
id="defs1034" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-93.242129"
|
||||
inkscape:cy="-189.9825"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1875"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="1965"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata1037">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(2.9219487,-8.5995374)">
|
||||
<path
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke-width:0.20233451"
|
||||
d="M 37.270955,98.335591 C 33.358064,97.07991 31.237736,92.52319 32.964256,89.08022 c 0.18139,-0.361738 4.757999,-5.096629 10.17021,-10.521968 l 9.84041,-9.864254 -4.03738,-4.041175 -4.037391,-4.041172 -4.96415,4.916665 c -3.61569,3.581096 -5.238959,5.04997 -5.975818,5.407377 l -1.011682,0.490718 H 17.267525 1.5866055 L 0.65034544,70.96512 C -2.2506745,69.535833 -3.5952145,66.18561 -2.5925745,62.884631 c 0.53525,-1.762217 1.61699004,-3.050074 3.22528014,-3.839847 l 1.15623996,-0.56778 13.2591094,-0.05613 13.259111,-0.05613 11.5262,-11.527539 11.526199,-11.527528 H 40.622647 c -12.145542,0 -12.189222,-0.0046 -13.752801,-1.445851 -2.229871,-2.055423 -2.162799,-5.970551 0.135998,-7.938238 1.475193,-1.262712 1.111351,-1.238469 18.588522,-1.238469 12.899229,0 16.035311,0.05193 16.692589,0.276494 0.641832,0.219264 2.590731,2.051402 9.416301,8.852134 l 8.606941,8.575638 h 6.848168 c 4.837422,0 7.092281,0.07311 7.679571,0.249094 0.48064,0.144008 1.22985,0.634863 1.77578,1.163429 2.383085,2.307333 1.968685,6.539886 -0.804989,8.221882 -0.571871,0.346781 -1.38284,0.687226 -1.80217,0.756523 -0.41933,0.06928 -4.2741,0.127016 -8.56615,0.128238 -6.56998,0.0016 -7.977492,-0.04901 -8.902732,-0.321921 -0.975569,-0.287742 -1.400468,-0.622236 -3.783999,-2.978832 l -2.685021,-2.654679 -5.05411,5.051071 -5.0541,5.051081 3.926292,3.947202 c 2.365399,2.378001 4.114289,4.309171 4.399158,4.857713 0.39266,0.75606 0.47311,1.219412 0.474321,2.731516 0.003,3.083647 0.620779,2.331942 -13.598011,16.531349 -10.273768,10.259761 -12.679778,12.563171 -13.500979,12.92519 -1.267042,0.55857 -3.156169,0.681342 -4.390271,0.285321 z m 40.130741,-65.45839 c -2.212909,-0.579748 -3.782711,-1.498393 -5.51275,-3.226063 -2.522111,-2.518633 -3.633121,-5.181304 -3.633121,-8.707194 0,-3.530699 1.11238,-6.197124 3.631161,-8.704043 4.866751,-4.8438383 12.324781,-4.8550953 17.211791,-0.026 3.908758,3.862461 4.818578,9.377999 2.372188,14.380771 -0.846209,1.730481 -3.39493,4.326384 -5.143839,5.239072 -2.69708,1.407492 -6.042829,1.798628 -8.92543,1.043434 z"
|
||||
id="path1000"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
67
docs/assets/index_user_guide.svg
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="123.72241mm"
|
||||
height="89.96759mm"
|
||||
viewBox="0 0 123.72242 89.96759"
|
||||
version="1.1"
|
||||
id="svg1040"
|
||||
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
|
||||
sodipodi:docname="index_userguide.svg">
|
||||
<defs
|
||||
id="defs1034" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="332.26618"
|
||||
inkscape:cy="83.744004"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="930"
|
||||
inkscape:window-height="472"
|
||||
inkscape:window-x="2349"
|
||||
inkscape:window-y="267"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata1037">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(141.8903,-20.32143)">
|
||||
<path
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke-width:0.20483544"
|
||||
d="m -139.53374,110.1657 c -0.80428,-0.24884 -1.71513,-1.11296 -2.07107,-1.96486 -0.23905,-0.57214 -0.28453,-6.28104 -0.28453,-35.720988 0,-38.274546 -0.079,-35.840728 1.19849,-36.91568 0.58869,-0.495345 4.63766,-2.187548 8.47998,-3.544073 l 1.58749,-0.560453 v -3.309822 c 0,-3.025538 0.0396,-3.388179 0.46086,-4.222122 0.68808,-1.362003 1.38671,-1.714455 4.60319,-2.322195 4.12797,-0.779966 5.13304,-0.912766 8.81544,-1.16476 11.80964,-0.808168 22.80911,2.509277 30.965439,9.3392 1.750401,1.465747 3.840861,3.5635 5.0903,5.108065 l 0.659122,0.814805 0.659109,-0.814805 c 1.249431,-1.544565 3.33988,-3.642318 5.09029,-5.108065 8.156331,-6.829923 19.155791,-10.147368 30.965441,-9.3392 3.682389,0.251994 4.68748,0.384794 8.81544,1.16476 3.21647,0.60774 3.91511,0.960192 4.60318,2.322195 0.4213,0.833943 0.46087,1.196584 0.46087,4.222122 v 3.309822 l 1.58748,0.560453 c 4.10165,1.448077 7.98852,3.072753 8.5259,3.563743 1.22643,1.120567 1.15258,-1.245868 1.15258,36.927177 0,34.567591 -0.005,35.083151 -0.40663,35.903991 -0.22365,0.45804 -0.73729,1.05665 -1.14143,1.33024 -1.22281,0.82783 -2.17721,0.70485 -5.86813,-0.7561 -9.19595,-3.63998 -18.956011,-6.38443 -26.791332,-7.53353 -3.02827,-0.44412 -9.26189,-0.61543 -11.77821,-0.3237 -5.19357,0.60212 -8.736108,2.05527 -11.700039,4.79936 -0.684501,0.63371 -1.466141,1.23646 -1.736979,1.33942 -0.63859,0.2428 -4.236521,0.2428 -4.875112,0 -0.27083,-0.10296 -1.05247,-0.70571 -1.73696,-1.33942 -2.96395,-2.74409 -6.50648,-4.19724 -11.700058,-4.79936 -2.516312,-0.29173 -8.749941,-0.12042 -11.778201,0.3237 -7.78194,1.14127 -17.39965,3.83907 -26.73341,7.49883 -3.38325,1.32658 -4.15525,1.50926 -5.11851,1.21125 z m 4.2107,-5.34052 c 5.86759,-2.29858 14.40398,-4.922695 20.2018,-6.210065 6.31584,-1.402418 8.5236,-1.646248 14.91592,-1.647338 4.68699,-7.94e-4 6.013661,0.0632 7.257809,0.3497 0.837332,0.19286 1.561052,0.312028 1.60828,0.264819 0.147111,-0.147119 -1.803289,-1.307431 -4.154879,-2.471801 -8.12511,-4.023029 -18.27311,-4.986568 -29.0861,-2.761718 -1.09536,0.22538 -2.32708,0.40827 -2.73715,0.406418 -1.12787,-0.005 -2.3054,-0.76382 -2.84516,-1.8332 l -0.46086,-0.913098 V 62.99179 35.97471 l -0.56331,0.138329 c -0.30981,0.07608 -1.89985,0.665075 -3.5334,1.308881 -2.27551,0.896801 -2.96414,1.252878 -2.94452,1.522563 0.014,0.193604 0.0372,15.284513 0.0512,33.535345 0.014,18.250839 0.0538,33.183322 0.0884,33.183322 0.0346,0 1.02543,-0.3771 2.20198,-0.83801 z m 113.006991,-32.697216 -0.0518,-33.535203 -3.17495,-1.272156 c -1.74623,-0.699685 -3.33627,-1.278755 -3.53341,-1.286819 -0.33966,-0.01389 -0.35847,1.401778 -0.35847,26.980216 v 26.994863 l -0.46087,0.913112 c -0.53976,1.06939 -1.71729,1.828088 -2.84515,1.833189 -0.41008,0.0021 -1.6418,-0.181031 -2.73716,-0.406421 -11.888201,-2.446089 -22.84337,-1.046438 -31.491022,4.02332 -1.68175,0.985941 -2.216748,1.467501 -1.36534,1.228942 1.575181,-0.441362 4.990592,-0.73864 8.524862,-0.742011 5.954408,-0.005 11.43046,0.791951 19.10874,2.78333 3.9516,1.024874 12.1555,3.687454 15.6699,5.085704 1.23926,0.49306 2.36869,0.90517 2.50985,0.9158 0.20489,0.0155 0.2462,-6.745894 0.20483,-33.515866 z m -59.76135,-2.233777 V 40.065438 l -0.95972,-1.357442 c -1.380522,-1.952627 -5.376262,-5.847994 -7.64336,-7.45136 -3.778692,-2.672401 -9.063392,-4.943324 -13.672511,-5.875304 -3.19731,-0.646503 -5.23069,-0.833103 -9.05886,-0.831312 -4.37716,0.0021 -7.70223,0.349169 -11.83461,1.235469 l -1.07538,0.230645 v 31.242342 c 0,26.565778 0.0426,31.226011 0.28429,31.133261 0.15637,-0.06 1.42379,-0.297169 2.81648,-0.527026 12.37657,-2.042634 23.21658,-0.346861 32.521639,5.087596 2.10018,1.226558 5.20202,3.618878 6.880942,5.30692 0.788609,0.792909 1.502978,1.446609 1.587468,1.452679 0.0845,0.006 0.153622,-13.411893 0.153622,-29.817719 z m 5.80221,28.3766 c 6.21476,-6.141601 15.08488,-10.061509 25.025529,-11.05933 4.262419,-0.427849 11.579921,-0.0054 16.017661,0.924912 0.75932,0.15916 1.45259,0.244888 1.54058,0.190498 0.088,-0.05434 0.16003,-14.060382 0.16003,-31.124436 V 26.176883 l -0.52136,-0.198219 c -0.66893,-0.254325 -4.77649,-0.95482 -7.159981,-1.221048 -2.41372,-0.269605 -8.559851,-0.266589 -10.759229,0.0052 -6.458111,0.798299 -12.584091,3.083792 -17.405651,6.49374 -2.267091,1.603366 -6.262831,5.498733 -7.64336,7.45136 l -0.959721,1.357438 v 29.828747 c 0,16.405812 0.0532,29.828746 0.11802,29.828746 0.065,0 0.77928,-0.65347 1.587482,-1.452149 z"
|
||||
id="path845"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="csscccscsssscsssssscscsccsccsccscsscccccccscccccccccsccscscscccscccsccssccsscccscccccsccccsccscsccsscc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
1
docs/assets/rocket_launch_48dp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="48px" viewBox="0 0 20 20" width="48px" fill="#000000"><g><rect fill="none" height="20" width="20" x="0"/></g><g><g><path d="M14.45,10.86l0.26,2.1c0.06,0.46-0.1,0.92-0.43,1.25l-3.55,3.55l-1.41-4.24l-2.83-2.83L2.25,9.27l3.55-3.55 c0.33-0.33,0.79-0.49,1.25-0.43l2.1,0.26C13.92,0.78,17.8,2.2,17.8,2.2C17.8,2.2,19.22,6.08,14.45,10.86z M10.2,6.62 C9.18,7.64,8.37,9.06,7.9,9.97l2.12,2.12c0.91-0.46,2.34-1.27,3.36-2.3C16.18,7,16.54,4.67,16.5,3.5C15.33,3.46,13,3.82,10.2,6.62 z M14,7.5C14,6.68,13.32,6,12.5,6C11.67,6,11,6.68,11,7.5S11.67,9,12.5,9C13.32,9,14,8.33,14,7.5z M7.99,6.92L6.86,6.78L5.02,8.61 l1.62,0.54C6.97,8.52,7.43,7.71,7.99,6.92z M10.85,13.36l0.54,1.62l1.83-1.83l-0.14-1.14C12.29,12.57,11.48,13.03,10.85,13.36z M5.25,12.5c-0.62,0-1.18,0.25-1.59,0.66C2.44,14.38,2,18,2,18s3.62-0.44,4.84-1.66c0.41-0.41,0.66-0.97,0.66-1.59 C7.5,13.51,6.49,12.5,5.25,12.5z M5.78,15.28c-0.44,0.44-1.87,0.81-1.87,0.81s0.37-1.43,0.81-1.87c0.29-0.29,0.77-0.29,1.06,0 S6.07,14.99,5.78,15.28z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -64,6 +64,7 @@ add_module_names = False # Remove namespaces from class/method signatures
|
||||
autodoc_inherit_docstrings = True # If no docstring, inherit from base class
|
||||
set_type_checking_flag = True # Enable 'expensive' imports for sphinx_autodoc_typehints
|
||||
autoclass_content = "both" # Include both class docstring and __init__
|
||||
autodoc_mock_imports = ["pyqtgraph"]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
@@ -76,6 +77,5 @@ language = "Python"
|
||||
|
||||
html_theme = "pydata_sphinx_theme"
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = ["css/custom.css"]
|
||||
html_logo = "_static/bec.png"
|
||||
html_theme_options = {"show_nav_level": 1, "navbar_align": "content"}
|
||||
html_css_files = ["custom.css"]
|
||||
html_logo = "../bec_widgets/assets/bec_widgets_icon.png"
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
(developer)=
|
||||
# Development
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 1
|
||||
hidden: true
|
||||
---
|
||||
reference/
|
||||
```
|
||||
|
||||
To contribute to the development of BEC Widgets, start by setting up the development environment:
|
||||
|
||||
1. **Clone the Repository**:
|
||||
|
||||
@@ -1,39 +1,70 @@
|
||||
# BEC Widgets documentation
|
||||
|
||||
A flexible and extensible framework for building graphical user interfaces in Python, optimized for use in the BEC environment.
|
||||
|
||||
<br><br>
|
||||
|
||||
````{grid} 3
|
||||
<br><br>
|
||||
|
||||
````{grid} 2
|
||||
:gutter: 5
|
||||
|
||||
```{grid-item-card} Introduction
|
||||
```{grid-item-card}
|
||||
:link: introduction
|
||||
:link-type: ref
|
||||
:img-top: /assets/index_getting_started.svg
|
||||
:text-align: center
|
||||
|
||||
General information.
|
||||
## Introduction
|
||||
|
||||
General information about BEC Widgets.
|
||||
```
|
||||
|
||||
```{grid-item-card} User
|
||||
```{grid-item-card}
|
||||
:link: user
|
||||
:link-type: ref
|
||||
:img-top: /assets/index_user_guide.svg
|
||||
:text-align: center
|
||||
|
||||
Information for users.
|
||||
## User guide
|
||||
|
||||
Information for users of BEC Widgets.
|
||||
```
|
||||
|
||||
```{grid-item-card} Developer
|
||||
```{grid-item-card}
|
||||
:link: developer
|
||||
:link-type: ref
|
||||
:img-top: /assets/index_contribute.svg
|
||||
:text-align: center
|
||||
|
||||
Information for developers.
|
||||
## Developer guide
|
||||
|
||||
Information for developers of BEC Widgets.
|
||||
```
|
||||
|
||||
```{grid-item-card}
|
||||
:link: api_reference
|
||||
:link-type: ref
|
||||
:img-top: /assets/index_api.svg
|
||||
:text-align: center
|
||||
|
||||
## API reference
|
||||
|
||||
Comprehensive reference of all BEC Widget classes, functions, and methods.
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
numbered: true
|
||||
maxdepth: 1
|
||||
maxdepth: 2
|
||||
hidden: true
|
||||
---
|
||||
|
||||
introduction/introduction
|
||||
user/user
|
||||
developer/developer
|
||||
api_reference/api_reference
|
||||
```
|
||||
|
||||
11
docs/user/api_reference/api_reference.md
Normal file
@@ -0,0 +1,11 @@
|
||||
(user.api_reference)=
|
||||
# User API Reference
|
||||
|
||||
```{eval-rst}
|
||||
.. autosummary::
|
||||
:toctree: _autosummary
|
||||
:template: custom-module-template.rst
|
||||
|
||||
bec_widgets.cli.client
|
||||
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
(user.apps)=
|
||||
(user.applications)=
|
||||
# Applications
|
||||
|
||||
**Coming soon**
|
||||
@@ -8,7 +8,3 @@
|
||||
maxdepth: 1
|
||||
hidden: true
|
||||
---
|
||||
|
||||
apps/motor_app
|
||||
apps/plot_app
|
||||
apps/modular_app
|
||||
@@ -1,8 +1,118 @@
|
||||
(user.customisation)=
|
||||
# Customisation
|
||||
|
||||
BEC Widgets are designed to be used with QtDesigner to quicly design GUI.
|
||||
## Leveraging BEC Widgets in custom GUI applications
|
||||
|
||||
BEC Widgets can be used to compose a complete Qt graphical application, along with
|
||||
other QWidgets. The only requirement is to connect to BEC servers in order to get
|
||||
data, or to interact with BEC components. This role is devoted to the BECDispatcher,
|
||||
a singleton object which has to be instantiated **after the QApplication is created**.
|
||||
|
||||
A typical BEC Widgets custom application "main" entry point should follow the template
|
||||
below:
|
||||
|
||||
```
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
# optional command line arguments processing
|
||||
parser = argparse.ArgumentParser(description="...")
|
||||
parser.add_argument( ...)
|
||||
...
|
||||
args = parser.parse_args()
|
||||
|
||||
# creation of the Qt application
|
||||
app = QApplication([])
|
||||
|
||||
# creation of BEC Dispatcher
|
||||
# /!\ important: after the QApplication has been instantiated
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
# (optional) processing of command line args,
|
||||
# creation of a main window depending on the command line arguments (or not)
|
||||
if args.xxx == "...":
|
||||
window = ...
|
||||
|
||||
# display of the main window and start of Qt event loop
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
```
|
||||
|
||||
The main "window" object presents the layout of widgets to the user and allows
|
||||
users to interact. BEC Widgets must be placed in the window:
|
||||
|
||||
```
|
||||
from qtpy.QWidgets import QMainWindow
|
||||
from bec_widgets.widgets import BECFigure
|
||||
|
||||
window = QMainWindow()
|
||||
bec_figure = BECFigure(gui_id="my_gui_app_id")
|
||||
window.setCentralWidget(bec_figure)
|
||||
|
||||
# prepare to plot samx motor vs bpm4i value
|
||||
bec_figure.plot(x_name="samx", y_name="bpm4i")
|
||||
```
|
||||
|
||||
In the example just above, the resulting application will show a plot of samx
|
||||
positions on the horizontal axis, and beam intensity on the vertical axis
|
||||
(when the next scan will be started).
|
||||
|
||||
It is important to ensure proper cleanup of the resources is done when application
|
||||
quits:
|
||||
|
||||
```
|
||||
def final_cleanup():
|
||||
bec_figure.clear_all()
|
||||
bec_figure.client.shutdown()
|
||||
|
||||
window.aboutToQuit.connect(final_cleanup)
|
||||
```
|
||||
|
||||
Final example:
|
||||
|
||||
```
|
||||
import sys
|
||||
from qtpy.QtWidgets import QMainWindow, QApplication
|
||||
from bec_widgets.widgets import BECFigure
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
# creation of the Qt application
|
||||
app = QApplication([])
|
||||
|
||||
# creation of BEC Dispatcher
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
# creation of main window
|
||||
window = QMainWindow()
|
||||
|
||||
# inserting BEC Widgets
|
||||
bec_figure = BECFigure(parent=window, gui_id="my_gui_app_id")
|
||||
window.setCentralWidget(bec_figure)
|
||||
|
||||
bec_figure.plot(x_name="samx", y_name="bpm4i")
|
||||
|
||||
# ensuring proper cleanup
|
||||
def final_cleanup():
|
||||
bec_figure.clear_all()
|
||||
bec_figure.client.shutdown()
|
||||
|
||||
app.aboutToQuit.connect(final_cleanup)
|
||||
|
||||
# execution
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
```
|
||||
|
||||
## Writing applications using Qt Designer
|
||||
|
||||
BEC Widgets are designed to be used with QtDesigner to quickly design GUI.
|
||||
|
||||
## Example of promoting widgets in Qt Designer
|
||||
|
||||
|
||||
BIN
docs/user/getting_started/BECDockArea.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
82
docs/user/getting_started/auto_updates.md
Normal file
@@ -0,0 +1,82 @@
|
||||
(user.auto_updates)=
|
||||
# Auto updates
|
||||
BEC Widgets provides a simple way to update the entire GUI configuration based on events. These events can be of different types, such as a new scan being started or completed, a button being pressed, a device reporting an error or the existence of a specific metadata key. This allows the users to streamline the experience of the GUI and to focus on the data and the analysis, rather than on the GUI itself.
|
||||
|
||||
The default auto update only takes control over a single `BECFigure` widget, which is automatically added to the GUI instance. The update instance is accessible via the `bec.gui.auto_updates` object. The user can disable / enable the auto updates by setting the `enabled` attribute of the `bec.gui.auto_updates` object, e.g.
|
||||
|
||||
```python
|
||||
bec.gui.auto_updates.enabled = False
|
||||
```
|
||||
|
||||
Without further customization, the auto update will automatically update the `BECFigure` widget based on the currently performed scan. The behaviour is determined by the `handler` method of the `AutoUpdate` class:
|
||||
|
||||
````{dropdown} Auto Updates Handler
|
||||
:icon: code-square
|
||||
:animate: fade-in-slide-down
|
||||
:open:
|
||||
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
|
||||
:pyobject: AutoUpdates.handler
|
||||
```
|
||||
````
|
||||
|
||||
As shown, the default handler switches between different scan names and updates the `BECFigure` widget accordingly. If the scan is a line scan, the `simple_line_scan` update method is executed.
|
||||
|
||||
````{dropdown} Auto Updates Simple Line Scan
|
||||
:icon: code-square
|
||||
:animate: fade-in-slide-down
|
||||
:open:
|
||||
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
|
||||
:pyobject: AutoUpdates.simple_line_scan
|
||||
```
|
||||
````
|
||||
|
||||
As it can be seen from the above snippet, the update method gets the default figure by calling the `get_default_figure` method. If the figure is not found, maybe because the user has deleted or closed it, no update is performed. If the figure is found, the scan info is used to extract the first reported device for the x axis and the first device of the monitored devices for the y axis. The y axis can also be set by the user using the `selected_device` attribute:
|
||||
|
||||
```python
|
||||
bec.gui.auto_updates.selected_device = 'bpm4i'
|
||||
```
|
||||
|
||||
|
||||
````{dropdown} Auto Updates Code
|
||||
:icon: code-square
|
||||
:animate: fade-in-slide-down
|
||||
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
|
||||
```
|
||||
````
|
||||
|
||||
## Custom Auto Updates
|
||||
The beamline can customize their default behaviour through customized auto update classes. This can be achieved by modifying the class located in the beamline plugin repository: `<beamline_plugin>/bec_widgets/auto_updates.py`. The class should inherit from the `AutoUpdates` class and overwrite the `handler` method.
|
||||
|
||||
```python
|
||||
from bec_widgets.cli.auto_updates import AutoUpdates, ScanInfo
|
||||
|
||||
|
||||
class PlotUpdate(AutoUpdates):
|
||||
create_default_dock = True
|
||||
enabled = True
|
||||
|
||||
# def simple_line_scan(self, info: ScanInfo) -> None:
|
||||
# """
|
||||
# Simple line scan.
|
||||
# """
|
||||
# fig = self.get_default_figure()
|
||||
# if not fig:
|
||||
# return
|
||||
# dev_x = info.scan_report_devices[0]
|
||||
# dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
|
||||
# if not dev_y:
|
||||
# return
|
||||
# fig.clear_all()
|
||||
# plt = fig.plot(x_name=dev_x, y_name=dev_y)
|
||||
# plt.set(title=f"Custom Plot {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
|
||||
def handler(self, info: ScanInfo) -> None:
|
||||
# EXAMPLES:
|
||||
# if info.scan_name == "line_scan" and info.scan_report_devices:
|
||||
# self.simple_line_scan(info)
|
||||
# return
|
||||
# if info.scan_name == "grid_scan" and info.scan_report_devices:
|
||||
# self.run_grid_scan_update(info)
|
||||
# return
|
||||
super().handler(info)
|
||||
```
|
||||
14
docs/user/getting_started/getting_started.md
Normal file
@@ -0,0 +1,14 @@
|
||||
(user.getting_started)=
|
||||
# Getting Started
|
||||
This section provides a comprehensive guide to getting started with BEC Widgets. Whether you are new to BEC Widgets or looking to refresh your knowledge, this guide will help you set up and navigate the framework with ease.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
hidden: true
|
||||
---
|
||||
|
||||
installation/
|
||||
quick_start/
|
||||
auto_updates/
|
||||
```
|
||||
BIN
docs/user/getting_started/gui_complex_gui.gif
Normal file
|
After Width: | Height: | Size: 6.2 MiB |
@@ -1,7 +1,5 @@
|
||||
(user.installation)=
|
||||
# Installation
|
||||
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
Before installing BEC Widgets, please ensure the following requirements are met:
|
||||
@@ -11,25 +9,14 @@ Before installing BEC Widgets, please ensure the following requirements are met:
|
||||
|
||||
**Standard Installation**
|
||||
|
||||
Install BEC Widgets using the pip package manager. Open your terminal and execute:
|
||||
To install BEC Widgets using the pip package manager, execute the following command in your terminal for getting the default PyQT6 version in your python environment:
|
||||
|
||||
```bash
|
||||
pip install bec_widgets
|
||||
```
|
||||
|
||||
This command installs BEC Widgets along with its dependencies, including the default PyQt6.
|
||||
|
||||
**Selecting a PyQt Version**
|
||||
|
||||
BEC Widgets supports both PyQt5 and PyQt6. To install a specific version, use:
|
||||
|
||||
For PyQt6:
|
||||
|
||||
```bash
|
||||
pip install bec_widgets[pyqt6]
|
||||
```
|
||||
|
||||
For PyQt5:
|
||||
In case you want to use PyQt5, you can install it by using the following command:
|
||||
|
||||
```bash
|
||||
pip install bec_widgets[pyqt5]
|
||||
110
docs/user/getting_started/quick_start.md
Normal file
@@ -0,0 +1,110 @@
|
||||
(user.command_line_introduction)=
|
||||
# Quick start
|
||||
In order to use BEC Widgets as a plotting tool for BEC, it needs to be [installed](#user.installation) in the same Python environment as the BEC IPython client (please refer to the [BEC documentation](https://bec.readthedocs.io/en/latest/user/command_line_interface.html#start-up) for more details). Upon startup, the client will automatically launch a GUI and store it as a `bec.gui` object in the client. The GUI backend will also be automatically connect to the BEC server, giving access to all information on the server and allowing the user to visualize the data in real-time.
|
||||
|
||||
## BECDockArea
|
||||
The `bec.gui` object is your entry point to BEC Widgets. It is a [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance that can be composed of multiple [`BECDock`](/api_reference/_autosummary/bec_widgets.cli.client.BECDock)s that can be attached / detached to the main area. These docks allow users to freely arrange and customize the widgets they add to the gui, providing a flexible and customizable interface to visualize data.
|
||||
|
||||
**Schema of the BECDockArea**
|
||||
|
||||

|
||||
|
||||
## Widgets
|
||||
Widgets are the building blocks of the BEC Widgets framework. They are the visual components that allow users to interact with the data and control the behavior of the application. Each dock can contain multiple widgets, albeit we recommend for most use cases a single widget per dock. BEC Widgets provides a set of core widgets (cf. [widgets](#user.widgets)). More widgets can be added by the users, and we invite you to explore the [developer documentation](developer.widgets) to learn how to create custom widgets.
|
||||
For the introduction given here, we will focus on the `BECFigure` widget, as it is the most commonly used widget for visualizing data from BEC. The same access pattern can be used for all other widgets.
|
||||
|
||||
**BECFigure**
|
||||
|
||||
The [`BECFigure`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure) widget is one of the core widgets developed for BEC and can be used to visualize different plot types, such as [1D waveforms](user.widgets.waveform_1d), [2D scatter plots](user.widgets.scatter_2d), [position maps](user.widgets.motor_map) and [2D images](user.widgets.image_2d).
|
||||
If BEC Widgets is installed, the default behaviour of BEC is to automatically add a BECFigure Widget to the existing GUI instance. This widget is directly accessible via the `fig` object from the client. Moreover, a best-effort attempt is made to automatically determine the best plot type based on the currently performed scan. This behaviour can be changed or disabled by the user. For more details, please refer to the [auto update](user.auto_updates) section.
|
||||
|
||||
<!-- We also provide two methods [`plot()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.plot), [`image()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.image) and [`motor_map()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.motor_map) as shortcuts to add a plot, image or motor map to the BECFigure. -->
|
||||
|
||||
**Waveform Plot**
|
||||
|
||||
The [`BECWaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform) is a widget that can be used to visualize 1D waveform data, i.e. to plot data of a monitor against a motor position. The method [`plot()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.plot) of BECFigure adds a BECWaveForm widget to the figure, and returns the plot object.
|
||||
|
||||
```python
|
||||
plt = fig.plot(x_name='samx', y_name='bpm4i')
|
||||
```
|
||||
Here, we create a new plot with a subscription to the devices `samx` and `bpm4i` and assign the plot to the object `plt`. We can now use this object to further customize the plot, e.g. changing the title ([`set_title()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_title)), axis labels ([`set_x_label()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_x_label)) or limits ([`set_x_lim()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_x_lim)). We invite you to explore the API of the BECWaveForm in the [documentation](user.widgets.waveform_1d) or directly in the command line.
|
||||
|
||||
To plot custom data, i.e. data that is not directly available through a scan in BEC, we can use the same method, but provide the data directly to the plot.
|
||||
|
||||
```python
|
||||
plt = fig.plot([1,2,3,4], [1,4,9,16])
|
||||
# or
|
||||
plt = fig.plot(x=[1,2,3,4], y=[1,4,9,16])
|
||||
# or
|
||||
plt = fig.plot(x=np.array([1,2,3,4]), y=np.array([1,4,9,16]))
|
||||
# or
|
||||
plt = fig.plot(np.random.rand(10,2))
|
||||
```
|
||||
|
||||
**Scatter Plot**
|
||||
|
||||
The [`BECWaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveForm) widget can also be used to visualize 2D scatter plots. More details on setting up the scatter plot are available in the widget documentation of the [scatter plot](user.widgets.scatter_2d).
|
||||
|
||||
**Motor Map**
|
||||
|
||||
The [`BECMotorMap`](/api_reference/_autosummary/bec_widgets.cli.client.BECMotorMap) widget can be used to visualize the position of motors. It's focused on tracking and visualizing the position of motors, crucial for precise alignment and movement tracking during scans. More details on setting up the motor map are available in the widget documentation of the [motor map](user.widgets.motor_map).
|
||||
|
||||
**Image Plot**
|
||||
|
||||
The [`BECImageItem`](/api_reference/_autosummary/bec_widgets.cli.client.BECImageItem) widget can be used to visualize 2D image data for example a camera. More details on setting up the image plot are available in the widget documentation of the [image plot](user.widgets.image).
|
||||
|
||||
### Useful Commands
|
||||
We recommend users to explore the API of the widgets by themselves since we assume that the user interface is supposed to be intuitive and self-explanatory. We appreciate feedback from user in order to constantly improve the experience and allow easy access to the gui, widgets and their functionality. We recommend checking the [API documentation](user.api_reference), but also by using BEC Widgets, exploring the available functions and check their dockstrings.
|
||||
```python
|
||||
gui.add_dock? # shows the dockstring of the add_dock method
|
||||
```
|
||||
|
||||
In addition, we list below a few useful commands that can be used to interface with the widgets:
|
||||
|
||||
```python
|
||||
gui.panels # returns a dictionary of all docks in the gui
|
||||
gui.add_dock() # adds a new dock to the gui
|
||||
|
||||
dock = gui.panels['dock_2']
|
||||
dock.add_widget('BECFigure') # adds a new widget of BECFigure to the dock
|
||||
dock.widget_list # returns a list of all widgets in the dock
|
||||
|
||||
figure = dock.widget_list[0] # assigns the created BECFigure to figure
|
||||
plt = figure.plot(x_name='samx', y_name='bpm4i') # adds a BECWaveForm plot to the figure
|
||||
plt.curves # returns a list of all curves in the plot
|
||||
```
|
||||
|
||||
We note that commands can also be chained. For example, `gui.add_dock().add_widget('BECFigure')` will add a new dock to the gui and add a new widget of `BECFigure` to the dock.
|
||||
|
||||
## Composing a larger GUI
|
||||
The example given above introduces BEC Widgets with its different components, and provides an overview of how to interact with the widgets. Nevertheless, another power aspect of BEC Widgets lies in the ability to compose a larger GUI with multiple docks and widgets. This section aims to provide a tutorial like guide on how to compose a more complex GUI that (A) live-plots a 1D waveform, (B) plots data from a camera, and (C) tracks the positions of two motors.
|
||||
Let's assume BEC was just started and the `gui` object is available in the client. A single dock is already attached together with a BEC Figure. Let's add the 1D waveform to this dock, change the color of the line to white and add the title *1D Waveform* to the plot.
|
||||
|
||||
```python
|
||||
plt = fig.plot(x_name='samx', y_name='bpm4i')
|
||||
plt.curves[0].set_color(color="white")
|
||||
plt.set_title('1D Waveform')
|
||||
```
|
||||
|
||||
Next, we add 2 new docks to the gui, one to plot the data of a camera and one to track the positions of two motors.
|
||||
```ipython
|
||||
cam_widget= gui.add_dock(name="cam_dock").add_widget('BECFigure').image("eiger")
|
||||
motor_widget = gui.add_dock(name="mot_dock").add_widget('BECFigure').motor_map("samx", "samy")
|
||||
```
|
||||
Note, we chain commands here which is possible since the `add_dock` and `add_widget` methods return the dock and the widget respectively. We can now further customize the widgets by changing the title, axis labels, etc.
|
||||
|
||||
```python
|
||||
cam_widget.set_title("Camera Image Eiger")
|
||||
cam_widget.set_vrange(vmin=0, vmax=100)
|
||||
```
|
||||
As a final step, we can now add also a SpiralProgressBar to a new dock, and perform a grid_scan with the motors *samx* and *samy*.
|
||||
As you see in the example below, all docks are arranged below each other. This is the default behavior of the `add_dock` method. However, the docks can be freely arranged by drag and drop as desired by the user. We invite you to explore this by yourself following the example in the video, and build your custom GUI with BEC Widgets.
|
||||
|
||||
```python
|
||||
prog_bar = gui.add_dock(name="prog_dock").add_widget('SpiralProgressBar')
|
||||
prog_bar.set_line_widths(15)
|
||||
scans.grid_scan(dev.samy, -2, 2, 10, dev.samx, -5, 5, 10, exp_time=0.1, relative=False)
|
||||
```
|
||||
|
||||

|
||||
|
||||
@@ -1,38 +1,67 @@
|
||||
(user)=
|
||||
# User
|
||||
|
||||
**Overview**
|
||||
|
||||
Welcome to the User section of the BEC Widgets documentation! BEC Widgets is a versatile GUI framework tailored for beamline scientists, enabling efficient and intuitive interaction with beamline experiments. This section is designed to guide both new and experienced users through the essential aspects of utilizing BEC Widgets.
|
||||
|
||||
**Key Topics**
|
||||
|
||||
- [Installing BEC Widgets](#user.installation): Instructions for installing BEC Widgets on your system.
|
||||
|
||||
- [Example Applications](#user.apps): Overview of bespoke applications and demonstrations of BEC Widgets in action, showcasing its use in real-world beamline scenarios.
|
||||
|
||||
- [Widgets Overview](#user.widgets): Detailed information on the variety of widgets available, their functions, and how to use them effectively.
|
||||
|
||||
- [Customization and Configuration](#user.customisation): Tips on customizing and configuring BEC Widgets to suit your specific experimental needs using Qt Designer.
|
||||
|
||||
**Bug Reports and Feature Requests**
|
||||
|
||||
We value your feedback and contributions to improving BEC Widgets. If you encounter any issues or have ideas for new features, we encourage you to report them.
|
||||
|
||||
- **Bug Reports:** If you find a bug or an issue, please report it on our repository's [Issues page](https://gitlab.psi.ch/bec/bec-widgets/-/issues?sort=created_date&state=opened). We have a template for bug reporting to help you provide all necessary information.
|
||||
- **Feature Requests:** Have an idea for a new feature or an enhancement? Share it with us on the [Issues page](https://gitlab.psi.ch/bec/bec-widgets/-/issues?sort=created_date&state=opened) of our repository. We have a feature request template that you can use to describe your proposal.
|
||||
|
||||
**Development**
|
||||
|
||||
For advanced details about BEC Widgets’ internal architecture, development contributions, or customization techniques, please explore the [Developer](#developer) section.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 3
|
||||
maxdepth: 2
|
||||
hidden: true
|
||||
---
|
||||
|
||||
installation
|
||||
apps
|
||||
widgets
|
||||
customisation
|
||||
getting_started/getting_started.md
|
||||
applications/applications.md
|
||||
widgets/widgets.md
|
||||
api_reference/api_reference.md
|
||||
```
|
||||
|
||||
|
||||
***
|
||||
|
||||
````{grid} 2
|
||||
:gutter: 5
|
||||
|
||||
```{grid-item-card}
|
||||
:link: user.getting_started
|
||||
:link-type: ref
|
||||
:img-top: /assets/rocket_launch_48dp.svg
|
||||
:text-align: center
|
||||
|
||||
## Getting Started
|
||||
|
||||
Learn how to install BEC Widgets and get started with the framework.
|
||||
```
|
||||
|
||||
```{grid-item-card}
|
||||
:link: user.applications
|
||||
:link-type: ref
|
||||
:img-top: /assets/display_settings_48dp.svg
|
||||
:text-align: center
|
||||
|
||||
## Applications
|
||||
|
||||
Learn how to use BEC Widgets Applications, to modify and create new applications.
|
||||
|
||||
```
|
||||
|
||||
```{grid-item-card}
|
||||
:link: user.widgets
|
||||
:link-type: ref
|
||||
:img-top: /assets/apps_48dp.svg
|
||||
:text-align: center
|
||||
|
||||
## Widgets
|
||||
|
||||
Learn about the building blocks of larger applications: widgets.
|
||||
```
|
||||
|
||||
```{grid-item-card}
|
||||
:link: user.api_reference
|
||||
:link-type: ref
|
||||
:img-top: /assets/index_api.svg
|
||||
:text-align: center
|
||||
|
||||
## API reference
|
||||
|
||||
Comprehensive reference of all user-facing classes, functions, and methods.
|
||||
```
|
||||
````
|
||||
@@ -1,41 +0,0 @@
|
||||
(user.widgets)=
|
||||
# Widgets
|
||||
|
||||
## Visualization Widgets
|
||||
|
||||
BEC Widgets includes a variety of visualization widgets designed to cater to diverse data representation needs in beamline experiments. These widgets enhance the user experience by providing intuitive and interactive data visualizations.
|
||||
|
||||
### 1D Waveform Widget
|
||||
|
||||
**Purpose:** This widget provides a straightforward visualization of 1D data. It is particularly useful for plotting positioner movements against detector readings, enabling users to observe correlations and patterns in a simple, linear format.
|
||||
|
||||
**Key Features:**
|
||||
- Real-time plotting of positioner versus detector values.
|
||||
- Interactive controls for zooming and panning through the data.
|
||||
- Customizable visual elements such as line color and style.
|
||||
|
||||
**Example of Use:**
|
||||

|
||||
### 2D Scatter Plot
|
||||
|
||||
**Purpose:** The 2D scatter plot widget is designed for more complex data visualization. It employs a false color map to represent a third dimension (z-axis), making it an ideal tool for visualizing multidimensional data sets.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- 2D scatter plot with color-coded data points based on a third variable (two positioners for x/y vs. one detector for colormap).
|
||||
- Interactive false color map for enhanced data interpretation.
|
||||
- Tools for selecting and inspecting specific data points.
|
||||
|
||||
**Example of Use:**
|
||||

|
||||
### Motor Position Map
|
||||
|
||||
**Purpose:** A specialized component derived from the Motor Alignment Tool. It's focused on tracking and visualizing the position of motors, crucial for precise alignment and movement tracking during scans.
|
||||
|
||||
**Key Features:**
|
||||
- Real-time tracking of motor positions.
|
||||
- Visual representation of motor trajectories, aiding in alignment tasks.
|
||||
- Ability to record and recall specific motor positions for repetitive tasks.
|
||||
|
||||
**Example of Use:**
|
||||

|
||||
BIN
docs/user/widgets/BECFigure.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
104
docs/user/widgets/bec_figure.md
Normal file
@@ -0,0 +1,104 @@
|
||||
(user.widgets.bec_figure)=
|
||||
# BECFigure
|
||||
[`BECFigure`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure) is a widget that provides a graphical user interface for creating and managing plots. It is a versatile tool that allows users to create a wide range of plots, from simple 1D waveforms to complex 2D scatter plots. BECFigure is designed to be user-friendly and interactive, enabling users to customize plots and visualize data in real-time.
|
||||
In the following, we describe 4 different type of widgets thaat are available in BECFigure.
|
||||
|
||||
**Schema of the BECFigure**
|
||||
|
||||

|
||||
|
||||
(user.widgets.waveform_1d)=
|
||||
## [1D Waveform Widget](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform)
|
||||
|
||||
**Purpose:** This widget provides a straightforward visualization of 1D data. It is particularly useful for plotting positioner movements against detector readings, enabling users to observe correlations and patterns in a simple, linear format.
|
||||
|
||||
**Key Features:**
|
||||
- Real-time plotting of positioner versus detector values.
|
||||
- Interactive controls for zooming and panning through the data.
|
||||
- Customizable visual elements such as line color and style.
|
||||
|
||||
**Example of Use:**
|
||||

|
||||
|
||||
**Code example**
|
||||
The following code snipped demonstrates how to create a 1D waveform plot using BEC Widgets within BEC. More details about BEC Widgets in BEC can be found in the getting started section within the [introduction to the command line.](user.command_line_introduction)
|
||||
```python
|
||||
# adds a new dock, a new BECFigure and a BECWaveForm to the dock
|
||||
plt = gui.add_dock().add_widget('BECFigure').plot('samx', 'bpm4i')
|
||||
# add a second curve to the same plot
|
||||
plt.plot(x_name='samx', y_name='bpm3i')
|
||||
plt.set_title("Gauss plots vs. samx")
|
||||
plt.set_x_label("Motor X")
|
||||
plt.set_y_label("Gauss Signal (A.U.")
|
||||
```
|
||||
Note, the return value of the simulated devices *bpm4i* and *bpm3i* may not be gaussian signals, but they can be easily configured with the code snippet below. For more details please check the documentation of the [simulation](https://bec.readthedocs.io/en/latest/developer/devices/bec_sim.html).
|
||||
```python
|
||||
# bpm4i uses GaussianModel and samx as a reference; default settings
|
||||
dev.bpm4i.sim.select_sim_model("GaussianModel")
|
||||
# bpm3i uses StepModel and samx as a reference; default settings
|
||||
dev.bpm3i.sim.select_sim_model("StepModel")
|
||||
```
|
||||
|
||||
(user.widgets.scatter_2d)=
|
||||
## [2D Scatter Plot](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform)
|
||||
|
||||
**Purpose:** The 2D scatter plot widget is designed for more complex data visualization. It employs a false color map to represent a third dimension (z-axis), making it an ideal tool for visualizing multidimensional data sets.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- 2D scatter plot with color-coded data points based on a third variable (two positioners for x/y vs. one detector for colormap).
|
||||
- Interactive false color map for enhanced data interpretation.
|
||||
- Tools for selecting and inspecting specific data points.
|
||||
|
||||
**Example of Use:**
|
||||

|
||||
|
||||
**Code example**
|
||||
The following code snipped demonstrates how to create a 2D scatter plot using BEC Widgets within BEC.
|
||||
```python
|
||||
# adds a new dock, a new BECFigure and a BECWaveForm to the dock
|
||||
plt = gui.add_dock().add_widget('BECFigure').add_plot(x_name='samx', y_name='samy', z_name='bpm4i')
|
||||
```
|
||||
|
||||
(user.widgets.motor_map)=
|
||||
## [Motor Position Map](/api_reference/_autosummary/bec_widgets.cli.client.BECMotorMap)
|
||||
|
||||
**Purpose:** A specialized component derived from the Motor Alignment Tool. It's focused on tracking and visualizing the position of motors, crucial for precise alignment and movement tracking during scans.
|
||||
|
||||
**Key Features:**
|
||||
- Real-time tracking of motor positions.
|
||||
- Visual representation of motor trajectories, aiding in alignment tasks.
|
||||
|
||||
**Example of Use:**
|
||||

|
||||
|
||||
**Code example**
|
||||
The following code snipped demonstrates how to create a motor map using BEC Widgets within BEC.
|
||||
```python
|
||||
# add a motor map to the gui
|
||||
mot_map = gui.add_dock().add_widget('BECFigure').motor_map('samx', 'samy')
|
||||
# change the number of points displayed
|
||||
```
|
||||
|
||||
(user.widgets.image_2d)=
|
||||
## [Image Plot](/api_reference/_autosummary/bec_widgets.cli.client.BECImageItem)
|
||||
|
||||
**Purpose:** A versatile widget for visualizing 2D image data, such as camera images. It provides a detailed representation of image data, with an attached color and scale bar to dynamically adjust the image display.
|
||||
|
||||
**Key Features:**
|
||||
- Live-plotting of 2D image data from cameras (*if data stream is available in BEC*).
|
||||
- Color maps and scale bars for customizing image display.
|
||||
**Example of Use:**
|
||||

|
||||
|
||||
**Code example**
|
||||
The following code snipped demonstrates how to create a motor map using BEC Widgets within BEC.
|
||||
```python
|
||||
# add a camera view for the eiger camera to the gui
|
||||
cam_widget = gui.add_dock().add_widget('BECFigure').image('eiger')
|
||||
# set the title of the camera view
|
||||
cam_widget.set_title("Camera Image Eiger")
|
||||
# change the color map range, e.g. from 0 to 100, per default it is autoscaling
|
||||
# Note, the simulation has hot pixels on the detector
|
||||
cam_widget.set_vrange(vmin=0, vmax=100)
|
||||
```
|
||||
38
docs/user/widgets/buttons.md
Normal file
@@ -0,0 +1,38 @@
|
||||
(user.widgets.buttons)=
|
||||
|
||||
# Buttons Widgets
|
||||
|
||||
This section consolidates various custom buttons used within the BEC GUIs, facilitating the integration of these
|
||||
controls into different layouts.
|
||||
|
||||
## Stop Button
|
||||
|
||||
**Purpose:**
|
||||
|
||||
The `Stop Button` provides a user interface control to immediately halt the execution of the current operation in the
|
||||
BEC Client. It is designed for easy integration into any BEC GUI layout.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **Immediate Termination:** Halts the execution of the current script or process.
|
||||
- **Queue Management:** Clears any pending operations in the scan queue, ensuring the system is ready for new tasks.
|
||||
|
||||
**Code example:**
|
||||
|
||||
Integrating the `StopButton` into a BEC GUI layout is straightforward. The following example demonstrates how to embed
|
||||
a `StopButton` within a GUI layout:
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QWidget, QVBoxLayout
|
||||
from bec_widgets.widgets import StopButton
|
||||
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
|
||||
|
||||
# Create and add the StopButton to the layout
|
||||
self.stop_button = StopButton()
|
||||
self.layout().addWidget(self.stop_button)
|
||||
```
|
||||
BIN
docs/user/widgets/image_plot.gif
Normal file
|
After Width: | Height: | Size: 13 MiB |
BIN
docs/user/widgets/progress_bar.gif
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
54
docs/user/widgets/spiral_progress_bar.md
Normal file
@@ -0,0 +1,54 @@
|
||||
(user.widgets.spiral_progress_bar)=
|
||||
# [Spiral Progress Bar](/api_reference/_autosummary/bec_widgets.cli.client.SpiralProgressBar)
|
||||
|
||||
**Purpose:**
|
||||
|
||||
The Spiral Progress Bar widget is a circular progress bar that can be used to visualize the progress of a task. The
|
||||
widget is designed to be used in applications where the progress of a task is represented as a percentage. The Spiral
|
||||
Progress Bar widget is a part of the BEC Widgets library and can be controlled directly using its API, or hooked up to
|
||||
the progress of a device readback or scan.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- circular progress bar to show updates on the progress of a task.
|
||||
- hooks to update the progress bar from a device readback or scan.
|
||||
- multiple progress rings to show different tasks in parallel.
|
||||
|
||||
**Example of Use:**
|
||||

|
||||
|
||||
**Code example:**
|
||||
|
||||
The following code snipped demonstrates how to create a `SpiralProgressBar` using BEC Widgets within BEC.
|
||||
```python
|
||||
# adds a new dock with a spiral progress bar
|
||||
progress = gui.add_dock().add_widget("SpiralProgressBar")
|
||||
# customize the size of the ring
|
||||
progress.set_line_width(20)
|
||||
```
|
||||
|
||||
By default, the Spiral Progress Bar widget will display a single ring. To add more rings, use the add_ring method:
|
||||
|
||||
```python
|
||||
# adds a new dock with a spiral progress bar
|
||||
progress.add_ring()
|
||||
```
|
||||
|
||||
To access rings and specify their properties, you can use `progress.rings` with an index specifying the ring index (
|
||||
starting from 0):
|
||||
|
||||
```python
|
||||
progress.rings[0].set_line_width(20) # set the width of the first ring
|
||||
progress.rings[1].set_line_width(10) # set the width of the second ring
|
||||
```
|
||||
|
||||
By default, the `SpiralProgressBar` widget is set with `progress.enable_auto_update(True)`, which will automatically
|
||||
update the bars in the widget. To manually set updates for each progress bar, use the set_update method. Note that
|
||||
manually updating a ring will disable the automatic update for the whole widget:
|
||||
|
||||
```python
|
||||
progress.rings[0].set_update("scan") # set the update of the first ring to be an overall scan progress
|
||||
progress.rings[1].set_update("device",
|
||||
"samx") # set the update of the second ring to be a device readback (in this case, samx)
|
||||
```
|
||||
|
||||
21
docs/user/widgets/website.md
Normal file
@@ -0,0 +1,21 @@
|
||||
(user.widgets.website)=
|
||||
# [Website Widget](/api_reference/_autosummary/bec_widgets.cli.client.WebsiteWidget)
|
||||
**Purpose:**
|
||||
|
||||
The Website Widget is a widget that allows you to display a website within the BEC GUI. The widget can be used to display any website.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- set the URL of the website to display.
|
||||
- reload the website.
|
||||
- navigate back and forward in the website history.
|
||||
|
||||
**Code example:**
|
||||
|
||||
The following code snipped demonstrates how to create a `WebsiteWidget` using BEC Widgets within BEC.
|
||||
```python
|
||||
# adds a new dock with a website widget
|
||||
web = gui.add_dock().add_widget("Website")
|
||||
# set the URL of the website to display
|
||||
web.set_url("https://bec.readthedocs.io/en/latest/")
|
||||
```
|
||||
17
docs/user/widgets/widgets.md
Normal file
@@ -0,0 +1,17 @@
|
||||
(user.widgets)=
|
||||
# Widgets
|
||||
BEC Widgets includes a variety of visualization widgets designed to cater to diverse data representation needs in beamline experiments. These widgets enhance the user experience by providing intuitive and interactive data visualizations.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
hidden: false
|
||||
---
|
||||
|
||||
bec_figure/
|
||||
spiral_progress_bar/
|
||||
website/
|
||||
buttons/
|
||||
```
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "0.56.0"
|
||||
version = "0.62.0"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
@@ -19,10 +19,13 @@ dependencies = [
|
||||
"qtpy",
|
||||
"pyqtgraph",
|
||||
"bec_lib",
|
||||
"bec_ipython_client", # needed for jupyter widget
|
||||
"zmq",
|
||||
"h5py",
|
||||
"pyqtdarktheme",
|
||||
"black",
|
||||
"black", # needed for bw-generate-cli
|
||||
"isort", # needed for bw-generate-cli
|
||||
"termqt @ git+ssh://git@github.com:TerryGeng/termqt.git"
|
||||
]
|
||||
|
||||
|
||||
@@ -31,19 +34,23 @@ dev = [
|
||||
"pytest",
|
||||
"pytest-random-order",
|
||||
"pytest-timeout",
|
||||
"pytest-xvfb",
|
||||
"coverage",
|
||||
"pytest-qt",
|
||||
"isort",
|
||||
"fakeredis",
|
||||
]
|
||||
pyqt5 = ["PyQt5>=5.9"]
|
||||
pyqt6 = ["PyQt6>=6.7"]
|
||||
pyqt5 = ["PyQt5>=5.9", "PyQtWebEngine>=5.9"]
|
||||
pyqt6 = ["PyQt6>=6.7", "PyQt6-WebEngine>=6.7"]
|
||||
pyside6 = ["PySide6>=6.7"]
|
||||
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://gitlab.psi.ch/bec/bec_widgets/issues"
|
||||
Homepage = "https://gitlab.psi.ch/bec/bec_widgets"
|
||||
|
||||
[project.scripts]
|
||||
bw-generate-cli = "bec_widgets.cli.generate_cli:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = ["*"]
|
||||
|
||||
@@ -56,6 +63,7 @@ profile = "black"
|
||||
line_length = 100
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = true
|
||||
known_first_party = ["bec_widgets"]
|
||||
|
||||
[tool.semantic_release]
|
||||
build_command = "python -m build"
|
||||
|
||||
@@ -1,40 +1,55 @@
|
||||
import pytest
|
||||
import random
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
|
||||
from bec_widgets.cli.client_utils import _start_plot_process
|
||||
from bec_widgets.cli.rpc_register import RPCRegister
|
||||
from bec_widgets.cli.server import BECWidgetsCLIServer
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.widgets import BECDockArea, BECFigure
|
||||
|
||||
|
||||
# make threads check in autouse, **will be executed at the end**; better than
|
||||
# having it in fixtures for each test, since it prevents from needing to
|
||||
# 'manually' shutdown bec_client_lib (for example) to make it happy, whereas
|
||||
# whereas in fact bec_client_lib makes its on cleanup
|
||||
@pytest.fixture(autouse=True)
|
||||
def rpc_register():
|
||||
yield RPCRegister()
|
||||
RPCRegister.reset_singleton()
|
||||
def threads_check_fixture(threads_check):
|
||||
return
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_server_figure(qtbot, bec_client_lib, threads_check):
|
||||
dispatcher = BECDispatcher(client=bec_client_lib) # Has to init singleton with fixture client
|
||||
server = BECWidgetsCLIServer(gui_id="figure", gui_class=BECFigure)
|
||||
qtbot.addWidget(server.gui)
|
||||
qtbot.waitExposed(server.gui)
|
||||
qtbot.wait(1000) # 1s long to wait until gui is ready
|
||||
yield server
|
||||
dispatcher.disconnect_all()
|
||||
server.client.shutdown()
|
||||
server.shutdown()
|
||||
dispatcher.reset_singleton()
|
||||
def gui_id():
|
||||
return f"figure_{random.randint(0,100)}" # make a new gui id each time, to ensure no 'gui is alive' zombie key can perturbate
|
||||
|
||||
|
||||
@contextmanager
|
||||
def plot_server(gui_id, klass, client_lib):
|
||||
dispatcher = BECDispatcher(client=client_lib) # Has to init singleton with fixture client
|
||||
process, output_thread = _start_plot_process(
|
||||
gui_id, klass, client_lib._client._service_config.redis
|
||||
)
|
||||
try:
|
||||
while client_lib._client.connector.get(MessageEndpoints.gui_heartbeat(gui_id)) is None:
|
||||
time.sleep(0.3)
|
||||
yield gui_id
|
||||
finally:
|
||||
process.terminate()
|
||||
process.wait()
|
||||
output_thread.join()
|
||||
dispatcher.disconnect_all()
|
||||
dispatcher.reset_singleton()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_server_dock(qtbot, bec_client_lib, threads_check):
|
||||
dispatcher = BECDispatcher(client=bec_client_lib) # Has to init singleton with fixture client
|
||||
server = BECWidgetsCLIServer(gui_id="figure", gui_class=BECDockArea)
|
||||
qtbot.addWidget(server.gui)
|
||||
qtbot.waitExposed(server.gui)
|
||||
qtbot.wait(1000) # 1s long to wait until gui is ready
|
||||
yield server
|
||||
dispatcher.disconnect_all()
|
||||
server.client.shutdown()
|
||||
server.shutdown()
|
||||
dispatcher.reset_singleton()
|
||||
def rpc_server_figure(gui_id, bec_client_lib):
|
||||
with plot_server(gui_id, BECFigure, bec_client_lib) as server:
|
||||
yield server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_server_dock(gui_id, bec_client_lib):
|
||||
with plot_server(gui_id, BECDockArea, bec_client_lib) as server:
|
||||
yield server
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
|
||||
from bec_widgets.cli.auto_updates import AutoUpdates
|
||||
from bec_widgets.cli.client import BECDockArea, BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||
from bec_widgets.utils import Colors
|
||||
|
||||
|
||||
def test_rpc_add_dock_with_figure_e2e(rpc_server_dock, qtbot):
|
||||
dock = BECDockArea(rpc_server_dock.gui_id)
|
||||
dock_server = rpc_server_dock.gui
|
||||
|
||||
def test_rpc_add_dock_with_figure_e2e(bec_client_lib, rpc_server_dock):
|
||||
# BEC client shortcuts
|
||||
client = rpc_server_dock.client
|
||||
dock = BECDockArea(rpc_server_dock)
|
||||
client = bec_client_lib
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
queue = client.queue
|
||||
@@ -21,17 +23,18 @@ def test_rpc_add_dock_with_figure_e2e(rpc_server_dock, qtbot):
|
||||
d1 = dock.add_dock("dock_1")
|
||||
d2 = dock.add_dock("dock_2")
|
||||
|
||||
assert len(dock_server.docks) == 3
|
||||
|
||||
dock_config = dock.config_dict
|
||||
assert len(dock_config["docks"]) == 3
|
||||
# Add 3 figures with some widgets
|
||||
fig0 = d0.add_widget_bec("BECFigure")
|
||||
fig1 = d1.add_widget_bec("BECFigure")
|
||||
fig2 = d2.add_widget_bec("BECFigure")
|
||||
fig0 = d0.add_widget("BECFigure")
|
||||
fig1 = d1.add_widget("BECFigure")
|
||||
fig2 = d2.add_widget("BECFigure")
|
||||
|
||||
assert len(dock_server.docks) == 3
|
||||
assert len(dock_server.docks["dock_0"].widgets) == 1
|
||||
assert len(dock_server.docks["dock_1"].widgets) == 1
|
||||
assert len(dock_server.docks["dock_2"].widgets) == 1
|
||||
dock_config = dock.config_dict
|
||||
assert len(dock_config["docks"]) == 3
|
||||
assert len(dock_config["docks"]["dock_0"]["widgets"]) == 1
|
||||
assert len(dock_config["docks"]["dock_1"]["widgets"]) == 1
|
||||
assert len(dock_config["docks"]["dock_2"]["widgets"]) == 1
|
||||
|
||||
assert fig1.__class__.__name__ == "BECFigure"
|
||||
assert fig1.__class__ == BECFigure
|
||||
@@ -84,7 +87,7 @@ def test_rpc_add_dock_with_figure_e2e(rpc_server_dock, qtbot):
|
||||
|
||||
# wait for scan to finish
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
time.sleep(0.2)
|
||||
|
||||
# plot
|
||||
plt_last_scan_data = queue.scan_storage.storage[-1].data
|
||||
@@ -96,7 +99,7 @@ def test_rpc_add_dock_with_figure_e2e(rpc_server_dock, qtbot):
|
||||
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
|
||||
"data"
|
||||
].data
|
||||
qtbot.wait(500)
|
||||
time.sleep(0.5)
|
||||
last_image_plot = im.images[0].get_data()
|
||||
np.testing.assert_equal(last_image_device, last_image_plot)
|
||||
|
||||
@@ -115,93 +118,91 @@ def test_rpc_add_dock_with_figure_e2e(rpc_server_dock, qtbot):
|
||||
)
|
||||
|
||||
|
||||
def test_dock_manipulations_e2e(rpc_server_dock, qtbot):
|
||||
dock = BECDockArea(rpc_server_dock.gui_id)
|
||||
dock_server = rpc_server_dock.gui
|
||||
def test_dock_manipulations_e2e(rpc_server_dock):
|
||||
dock = BECDockArea(rpc_server_dock)
|
||||
|
||||
d0 = dock.add_dock("dock_0")
|
||||
d1 = dock.add_dock("dock_1")
|
||||
d2 = dock.add_dock("dock_2")
|
||||
assert len(dock_server.docks) == 3
|
||||
dock_config = dock.config_dict
|
||||
assert len(dock_config["docks"]) == 3
|
||||
|
||||
d0.detach()
|
||||
dock.detach_dock("dock_2")
|
||||
assert len(dock_server.docks) == 3
|
||||
assert len(dock_server.tempAreas) == 2
|
||||
dock_config = dock.config_dict
|
||||
assert len(dock_config["docks"]) == 3
|
||||
assert len(dock.temp_areas) == 2
|
||||
|
||||
d0.attach()
|
||||
assert len(dock_server.docks) == 3
|
||||
assert len(dock_server.tempAreas) == 1
|
||||
dock_config = dock.config_dict
|
||||
assert len(dock_config["docks"]) == 3
|
||||
assert len(dock.temp_areas) == 1
|
||||
|
||||
d2.remove()
|
||||
qtbot.wait(200)
|
||||
dock_config = dock.config_dict
|
||||
assert len(dock_config["docks"]) == 2
|
||||
|
||||
assert len(dock_server.docks) == 2
|
||||
docks_list = list(dict(dock_server.docks).keys())
|
||||
assert ["dock_0", "dock_1"] == docks_list
|
||||
assert ["dock_0", "dock_1"] == list(dock_config["docks"])
|
||||
|
||||
dock.clear_all()
|
||||
|
||||
assert len(dock_server.docks) == 0
|
||||
assert len(dock_server.tempAreas) == 0
|
||||
dock_config = dock.config_dict
|
||||
assert len(dock_config["docks"]) == 0
|
||||
assert len(dock.temp_areas) == 0
|
||||
|
||||
|
||||
def test_spiral_bar(rpc_server_dock):
|
||||
dock = BECDockArea(rpc_server_dock.gui_id)
|
||||
dock_server = rpc_server_dock.gui
|
||||
dock = BECDockArea(rpc_server_dock)
|
||||
|
||||
d0 = dock.add_dock(name="dock_0")
|
||||
|
||||
bar = d0.add_widget_bec("SpiralProgressBar")
|
||||
bar = d0.add_widget("SpiralProgressBar")
|
||||
assert bar.__class__.__name__ == "SpiralProgressBar"
|
||||
|
||||
bar.set_number_of_bars(5)
|
||||
bar.set_colors_from_map("viridis")
|
||||
bar.set_value([10, 20, 30, 40, 50])
|
||||
|
||||
bar_server = dock_server.docks["dock_0"].widgets[0]
|
||||
bar_config = bar.config_dict
|
||||
|
||||
expected_colors = Colors.golden_angle_color("viridis", 5, "RGB")
|
||||
bar_colors = [ring.color.getRgb() for ring in bar_server.rings]
|
||||
bar_values = [ring.value for ring in bar_server.rings]
|
||||
expected_colors = [list(color) for color in Colors.golden_angle_color("viridis", 5, "RGB")]
|
||||
bar_colors = [ring.config_dict["color"] for ring in bar.rings]
|
||||
bar_values = [ring.config_dict["value"] for ring in bar.rings]
|
||||
assert bar_config["num_bars"] == 5
|
||||
assert bar_values == [10, 20, 30, 40, 50]
|
||||
assert bar_colors == expected_colors
|
||||
|
||||
|
||||
def test_spiral_bar_scan_update(rpc_server_dock, qtbot):
|
||||
dock = BECDockArea(rpc_server_dock.gui_id)
|
||||
dock_server = rpc_server_dock.gui
|
||||
def test_spiral_bar_scan_update(bec_client_lib, rpc_server_dock):
|
||||
dock = BECDockArea(rpc_server_dock)
|
||||
|
||||
d0 = dock.add_dock("dock_0")
|
||||
|
||||
d0.add_widget_bec("SpiralProgressBar")
|
||||
bar = d0.add_widget("SpiralProgressBar")
|
||||
|
||||
client = rpc_server_dock.client
|
||||
client = bec_client_lib
|
||||
dev = client.device_manager.devices
|
||||
dev.samx.tolerance.set(0)
|
||||
dev.samy.tolerance.set(0)
|
||||
scans = client.scans
|
||||
|
||||
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
status.wait()
|
||||
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
|
||||
qtbot.wait(200)
|
||||
bar_server = dock_server.docks["dock_0"].widgets[0]
|
||||
assert bar_server.config.num_bars == 1
|
||||
np.testing.assert_allclose(bar_server.rings[0].value, 10, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[0].config.min_value, 0, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[0].config.max_value, 10, atol=0.1)
|
||||
bar_config = bar.config_dict
|
||||
assert bar_config["num_bars"] == 1
|
||||
assert bar_config["rings"][0]["value"] == 10
|
||||
assert bar_config["rings"][0]["min_value"] == 0
|
||||
assert bar_config["rings"][0]["max_value"] == 10
|
||||
|
||||
status = scans.grid_scan(dev.samx, -5, 5, 4, dev.samy, -10, 10, 4, relative=True, exp_time=0.1)
|
||||
status.wait()
|
||||
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
|
||||
qtbot.wait(200)
|
||||
assert bar_server.config.num_bars == 1
|
||||
np.testing.assert_allclose(bar_server.rings[0].value, 16, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[0].config.min_value, 0, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[0].config.max_value, 16, atol=0.1)
|
||||
bar_config = bar.config_dict
|
||||
assert bar_config["num_bars"] == 1
|
||||
assert bar_config["rings"][0]["value"] == 16
|
||||
assert bar_config["rings"][0]["min_value"] == 0
|
||||
assert bar_config["rings"][0]["max_value"] == 16
|
||||
|
||||
init_samx = dev.samx.read()["samx"]["value"]
|
||||
init_samy = dev.samy.read()["samy"]["value"]
|
||||
@@ -212,15 +213,72 @@ def test_spiral_bar_scan_update(rpc_server_dock, qtbot):
|
||||
dev.samy.velocity.put(5)
|
||||
|
||||
status = scans.umv(dev.samx, 5, dev.samy, 10, relative=True)
|
||||
status.wait()
|
||||
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
bar_config = bar.config_dict
|
||||
assert bar_config["num_bars"] == 2
|
||||
assert bar_config["rings"][0]["value"] == final_samx
|
||||
assert bar_config["rings"][1]["value"] == final_samy
|
||||
assert bar_config["rings"][0]["min_value"] == init_samx
|
||||
assert bar_config["rings"][0]["max_value"] == final_samx
|
||||
assert bar_config["rings"][1]["min_value"] == init_samy
|
||||
assert bar_config["rings"][1]["max_value"] == final_samy
|
||||
|
||||
qtbot.wait(200)
|
||||
assert bar_server.config.num_bars == 2
|
||||
np.testing.assert_allclose(bar_server.rings[0].value, final_samx, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[1].value, final_samy, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[0].config.min_value, init_samx, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[1].config.min_value, init_samy, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[0].config.max_value, final_samx, atol=0.1)
|
||||
np.testing.assert_allclose(bar_server.rings[1].config.max_value, final_samy, atol=0.1)
|
||||
|
||||
def test_auto_update(bec_client_lib, rpc_server_dock):
|
||||
dock = BECDockArea(rpc_server_dock)
|
||||
|
||||
AutoUpdates.enabled = True
|
||||
AutoUpdates.create_default_dock = True
|
||||
dock.auto_updates = AutoUpdates(gui=dock)
|
||||
dock.auto_updates.start_default_dock()
|
||||
dock.selected_device = "bpm4i"
|
||||
|
||||
# we need to start the update script manually; normally this is done when the GUI is started
|
||||
dock._start_update_script()
|
||||
|
||||
client = bec_client_lib
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
queue = client.queue
|
||||
|
||||
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
status.wait()
|
||||
|
||||
last_scan_data = queue.scan_storage.storage[-1].data
|
||||
|
||||
# get data from curves
|
||||
plt = dock.auto_updates.get_default_figure()
|
||||
widgets = plt.widget_list
|
||||
plt_data = widgets[0].get_all_data()
|
||||
|
||||
# check plotted data
|
||||
assert (
|
||||
plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["x"]
|
||||
== last_scan_data["samx"]["samx"].val
|
||||
)
|
||||
assert (
|
||||
plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["y"]
|
||||
== last_scan_data["bpm4i"]["bpm4i"].val
|
||||
)
|
||||
|
||||
status = scans.grid_scan(
|
||||
dev.samx, -10, 10, 5, dev.samy, -5, 5, 5, exp_time=0.05, relative=False
|
||||
)
|
||||
status.wait()
|
||||
|
||||
plt = dock.auto_updates.get_default_figure()
|
||||
widgets = plt.widget_list
|
||||
plt_data = widgets[0].get_all_data()
|
||||
|
||||
last_scan_data = queue.scan_storage.storage[-1].data
|
||||
|
||||
# check plotted data
|
||||
assert (
|
||||
plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["x"]
|
||||
== last_scan_data["samx"]["samx"].val
|
||||
)
|
||||
assert (
|
||||
plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["y"]
|
||||
== last_scan_data["samy"]["samy"].val
|
||||
)
|
||||
|
||||
@@ -5,31 +5,29 @@ from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||
|
||||
|
||||
def test_rpc_waveform1d_custom_curve(rpc_server_figure, qtbot):
|
||||
fig = BECFigure(rpc_server_figure.gui_id)
|
||||
fig_server = rpc_server_figure.gui
|
||||
def test_rpc_waveform1d_custom_curve(rpc_server_figure):
|
||||
fig = BECFigure(rpc_server_figure)
|
||||
|
||||
ax = fig.add_plot()
|
||||
curve = ax.add_curve_custom([1, 2, 3], [1, 2, 3])
|
||||
curve = ax.plot(x=[1, 2, 3], y=[1, 2, 3])
|
||||
curve.set_color("red")
|
||||
curve = ax.curves[0]
|
||||
curve.set_color("blue")
|
||||
|
||||
assert len(fig_server.widgets) == 1
|
||||
assert len(fig_server.widgets[ax.rpc_id].curves) == 1
|
||||
assert len(fig.widgets) == 1
|
||||
assert len(fig.widgets[ax.rpc_id].curves) == 1
|
||||
|
||||
|
||||
def test_rpc_plotting_shortcuts_init_configs(rpc_server_figure, qtbot):
|
||||
fig = BECFigure(rpc_server_figure.gui_id)
|
||||
fig_server = rpc_server_figure.gui
|
||||
fig = BECFigure(rpc_server_figure)
|
||||
|
||||
plt = fig.plot(x_name="samx", y_name="bpm4i")
|
||||
im = fig.image("eiger")
|
||||
motor_map = fig.motor_map("samx", "samy")
|
||||
plt_z = fig.add_plot("samx", "samy", "bpm4i")
|
||||
plt_z = fig.add_plot(x_name="samx", y_name="samy", z_name="bpm4i")
|
||||
|
||||
# Checking if classes are correctly initialised
|
||||
assert len(fig_server.widgets) == 4
|
||||
assert len(fig.widgets) == 4
|
||||
assert plt.__class__.__name__ == "BECWaveform"
|
||||
assert plt.__class__ == BECWaveform
|
||||
assert im.__class__.__name__ == "BECImageShow"
|
||||
@@ -75,24 +73,21 @@ def test_rpc_plotting_shortcuts_init_configs(rpc_server_figure, qtbot):
|
||||
}
|
||||
|
||||
|
||||
def test_rpc_waveform_scan(rpc_server_figure, qtbot):
|
||||
fig = BECFigure(rpc_server_figure.gui_id)
|
||||
def test_rpc_waveform_scan(rpc_server_figure, bec_client_lib):
|
||||
fig = BECFigure(rpc_server_figure)
|
||||
|
||||
# add 3 different curves to track
|
||||
plt = fig.plot(x_name="samx", y_name="bpm4i")
|
||||
fig.plot(x_name="samx", y_name="bpm3a")
|
||||
fig.plot(x_name="samx", y_name="bpm4d")
|
||||
|
||||
client = rpc_server_figure.client
|
||||
client = bec_client_lib
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
queue = client.queue
|
||||
|
||||
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
|
||||
# wait for scan to finish
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
status.wait()
|
||||
|
||||
last_scan_data = queue.scan_storage.storage[-1].data
|
||||
|
||||
@@ -108,38 +103,33 @@ def test_rpc_waveform_scan(rpc_server_figure, qtbot):
|
||||
assert plt_data["bpm4d-bpm4d"]["y"] == last_scan_data["bpm4d"]["bpm4d"].val
|
||||
|
||||
|
||||
def test_rpc_image(rpc_server_figure, qtbot):
|
||||
fig = BECFigure(rpc_server_figure.gui_id)
|
||||
def test_rpc_image(rpc_server_figure, bec_client_lib):
|
||||
fig = BECFigure(rpc_server_figure)
|
||||
|
||||
im = fig.image("eiger")
|
||||
|
||||
client = rpc_server_figure.client
|
||||
client = bec_client_lib
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
|
||||
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
|
||||
# wait for scan to finish
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
status.wait()
|
||||
|
||||
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
|
||||
"data"
|
||||
].data
|
||||
qtbot.wait(500)
|
||||
last_image_plot = im.images[0].get_data()
|
||||
|
||||
# check plotted data
|
||||
np.testing.assert_equal(last_image_device, last_image_plot)
|
||||
|
||||
|
||||
def test_rpc_motor_map(rpc_server_figure, qtbot):
|
||||
fig = BECFigure(rpc_server_figure.gui_id)
|
||||
fig_server = rpc_server_figure.gui
|
||||
def test_rpc_motor_map(rpc_server_figure, bec_client_lib):
|
||||
fig = BECFigure(rpc_server_figure)
|
||||
|
||||
motor_map = fig.motor_map("samx", "samy")
|
||||
|
||||
client = rpc_server_figure.client
|
||||
client = bec_client_lib
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
|
||||
@@ -147,10 +137,8 @@ def test_rpc_motor_map(rpc_server_figure, qtbot):
|
||||
initial_pos_y = dev.samy.read()["samy"]["value"]
|
||||
|
||||
status = scans.mv(dev.samx, 1, dev.samy, 2, relative=True)
|
||||
status.wait()
|
||||
|
||||
# wait for scan to finish
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
final_pos_x = dev.samx.read()["samx"]["value"]
|
||||
final_pos_y = dev.samy.read()["samy"]["value"]
|
||||
|
||||
|
||||
@@ -3,40 +3,30 @@ import pytest
|
||||
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||
|
||||
|
||||
def find_deepest_value(d: dict):
|
||||
"""
|
||||
Recursively find the deepest value in a dictionary
|
||||
Args:
|
||||
d(dict): Dictionary to search
|
||||
|
||||
Returns:
|
||||
The deepest value in the dictionary.
|
||||
"""
|
||||
if isinstance(d, dict):
|
||||
if d:
|
||||
return find_deepest_value(next(iter(d.values())))
|
||||
return d
|
||||
|
||||
|
||||
def test_rpc_register_list_connections(rpc_server_figure, rpc_register, qtbot):
|
||||
fig = BECFigure(rpc_server_figure.gui_id)
|
||||
fig_server = rpc_server_figure.gui
|
||||
def test_rpc_register_list_connections(rpc_server_figure):
|
||||
fig = BECFigure(rpc_server_figure)
|
||||
|
||||
plt = fig.plot(x_name="samx", y_name="bpm4i")
|
||||
im = fig.image("eiger")
|
||||
motor_map = fig.motor_map("samx", "samy")
|
||||
plt_z = fig.add_plot("samx", "samy", "bpm4i")
|
||||
plt_z = fig.add_plot(x_name="samx", y_name="samy", z_name="bpm4i")
|
||||
|
||||
all_connections = rpc_register.list_all_connections()
|
||||
# keep only class names from objects, since objects on server and client are different
|
||||
# so the best we can do is to compare types (rpc register is unit-tested elsewhere)
|
||||
all_connections = {obj_id: type(obj).__name__ for obj_id, obj in fig.get_all_rpc().items()}
|
||||
|
||||
# Construct dict of all rpc items manually
|
||||
all_subwidgets_expected = dict(fig_server.widgets)
|
||||
curve_1D = find_deepest_value(fig_server.widgets[plt.rpc_id]._curves_data)
|
||||
curve_2D = find_deepest_value(fig_server.widgets[plt_z.rpc_id]._curves_data)
|
||||
curves_expected = {curve_1D.rpc_id: curve_1D, curve_2D.rpc_id: curve_2D}
|
||||
fig_expected = {fig.rpc_id: fig_server}
|
||||
all_subwidgets_expected = {wid: type(widget).__name__ for wid, widget in fig.widgets.items()}
|
||||
curve_1D = fig.widgets[plt.rpc_id]
|
||||
curve_2D = fig.widgets[plt_z.rpc_id]
|
||||
curves_expected = {
|
||||
curve_1D.rpc_id: type(curve_1D).__name__,
|
||||
curve_2D.rpc_id: type(curve_2D).__name__,
|
||||
}
|
||||
curves_expected.update({curve._gui_id: type(curve).__name__ for curve in curve_1D.curves})
|
||||
curves_expected.update({curve._gui_id: type(curve).__name__ for curve in curve_2D.curves})
|
||||
fig_expected = {fig.rpc_id: type(fig).__name__}
|
||||
image_item_expected = {
|
||||
fig_server.widgets[im.rpc_id].images[0].rpc_id: fig_server.widgets[im.rpc_id].images[0]
|
||||
fig.widgets[im.rpc_id].images[0].rpc_id: type(fig.widgets[im.rpc_id].images[0]).__name__
|
||||
}
|
||||
|
||||
all_connections_expected = {
|
||||
|
||||
@@ -58,7 +58,7 @@ def test_bec_dock_area_add_remove_dock(bec_dock_area, qtbot):
|
||||
|
||||
def test_add_remove_bec_figure_to_dock(bec_dock_area):
|
||||
d0 = bec_dock_area.add_dock()
|
||||
fig = d0.add_widget_bec("BECFigure")
|
||||
fig = d0.add_widget("BECFigure")
|
||||
plt = fig.plot(x_name="samx", y_name="bpm4i")
|
||||
im = fig.image("eiger")
|
||||
mm = fig.motor_map("samx", "samy")
|
||||
|
||||
@@ -25,5 +25,5 @@ def test_rpc_call_accepts_device_as_input(cli_figure):
|
||||
dev1 = FakeDevice("samx")
|
||||
dev2 = FakeDevice("bpm4i")
|
||||
fig, mock_rpc_call = cli_figure
|
||||
fig.plot(dev1, dev2)
|
||||
mock_rpc_call.assert_called_with("plot", "samx", "bpm4i")
|
||||
fig.plot(x_name=dev1, y_name=dev2)
|
||||
mock_rpc_call.assert_called_with("plot", x_name="samx", y_name="bpm4i")
|
||||
|
||||
60
tests/unit_tests/test_color_validation.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import CurveConfig
|
||||
|
||||
|
||||
def test_color_validation_CSS():
|
||||
# Test valid color
|
||||
color = Colors.validate_color("teal")
|
||||
assert color == "teal"
|
||||
|
||||
# Test invalid color
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
CurveConfig(color="invalid_color")
|
||||
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == ("unsupported color")
|
||||
assert "The color must be a valid HEX string or CSS Color." in str(excinfo.value)
|
||||
|
||||
|
||||
def test_color_validation_hex():
|
||||
# Test valid color
|
||||
color = Colors.validate_color("#ff0000")
|
||||
assert color == "#ff0000"
|
||||
|
||||
# Test invalid color
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
CurveConfig(color="#ff00000")
|
||||
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == ("unsupported color")
|
||||
assert "The color must be a valid HEX string or CSS Color." in str(excinfo.value)
|
||||
|
||||
|
||||
def test_color_validation_RGBA():
|
||||
# Test valid color
|
||||
color = Colors.validate_color((255, 0, 0, 255))
|
||||
assert color == (255, 0, 0, 255)
|
||||
|
||||
# Test invalid color
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
CurveConfig(color=(255, 0, 0))
|
||||
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == ("unsupported color")
|
||||
assert "The color must be a tuple of 4 elements (R, G, B, A)." in str(excinfo.value)
|
||||
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
CurveConfig(color=(255, 0, 0, 355))
|
||||
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == ("unsupported color")
|
||||
assert "The color values must be between 0 and 255 in RGBA format (R,G,B,A)" in str(
|
||||
excinfo.value
|
||||
)
|
||||
@@ -1,10 +1,12 @@
|
||||
from textwrap import dedent
|
||||
|
||||
import black
|
||||
import pytest
|
||||
import isort
|
||||
|
||||
from bec_widgets.cli.generate_cli import ClientGenerator
|
||||
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
|
||||
# Mock classes to test the generator
|
||||
class MockBECWaveform1D:
|
||||
@@ -24,25 +26,39 @@ class MockBECFigure:
|
||||
|
||||
def add_plot(self, plot_id: str):
|
||||
"""Add a plot to the figure."""
|
||||
pass
|
||||
|
||||
def remove_plot(self, plot_id: str):
|
||||
"""Remove a plot from the figure."""
|
||||
pass
|
||||
|
||||
|
||||
def test_client_generator_with_black_formatting():
|
||||
generator = ClientGenerator()
|
||||
generator.generate_client([MockBECWaveform1D, MockBECFigure])
|
||||
rpc_classes = {
|
||||
"connector_classes": [MockBECWaveform1D, MockBECFigure],
|
||||
"top_level_classes": [MockBECFigure],
|
||||
}
|
||||
generator.generate_client(rpc_classes)
|
||||
|
||||
# Format the expected output with black to ensure it matches the generator output
|
||||
expected_output = dedent(
|
||||
'''\
|
||||
# This file was automatically generated by generate_cli.py
|
||||
|
||||
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECGuiClientMixin
|
||||
import enum
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
from bec_widgets.cli.client_utils import BECGuiClientMixin, RPCBase, rpc_call
|
||||
|
||||
# pylint: skip-file
|
||||
|
||||
class Widgets(str, enum.Enum):
|
||||
"""
|
||||
Enum for the available widgets.
|
||||
"""
|
||||
|
||||
MockBECFigure = "MockBECFigure"
|
||||
|
||||
|
||||
class MockBECWaveform1D(RPCBase):
|
||||
@rpc_call
|
||||
def set_frequency(self, frequency: float) -> list:
|
||||
@@ -78,4 +94,20 @@ def test_client_generator_with_black_formatting():
|
||||
generator.header + "\n" + generator.content, mode=black.FileMode(line_length=100)
|
||||
)
|
||||
|
||||
generated_output_formatted = isort.code(generated_output_formatted)
|
||||
|
||||
assert expected_output_formatted == generated_output_formatted
|
||||
|
||||
|
||||
def test_client_generator_classes():
|
||||
generator = ClientGenerator()
|
||||
out = generator.get_rpc_classes("bec_widgets")
|
||||
assert list(out.keys()) == ["connector_classes", "top_level_classes"]
|
||||
connector_cls_names = [cls.__name__ for cls in out["connector_classes"]]
|
||||
top_level_cls_names = [cls.__name__ for cls in out["top_level_classes"]]
|
||||
|
||||
assert "BECFigure" in connector_cls_names
|
||||
assert "BECWaveform" in connector_cls_names
|
||||
assert "BECDockArea" in top_level_cls_names
|
||||
assert "BECFigure" in top_level_cls_names
|
||||
assert "BECWaveform" not in top_level_cls_names
|
||||
|
||||
@@ -145,12 +145,12 @@ def test_bar_set_value(spiral_progress_bar):
|
||||
assert len(spiral_progress_bar.rings) == 5
|
||||
|
||||
spiral_progress_bar.set_value([10, 20, 30, 40, 50])
|
||||
ring_values = [ring.value for ring in spiral_progress_bar.rings]
|
||||
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
|
||||
assert ring_values == [10, 20, 30, 40, 50]
|
||||
|
||||
# update just one bar
|
||||
spiral_progress_bar.set_value(90, 1)
|
||||
ring_values = [ring.value for ring in spiral_progress_bar.rings]
|
||||
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
|
||||
assert ring_values == [10, 90, 30, 40, 50]
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ def test_bar_set_precision(spiral_progress_bar):
|
||||
assert ring_precision == [2, 2, 2]
|
||||
|
||||
spiral_progress_bar.set_value([10.1234, 20.1234, 30.1234])
|
||||
ring_values = [ring.value for ring in spiral_progress_bar.rings]
|
||||
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
|
||||
assert ring_values == [10.12, 20.12, 30.12]
|
||||
|
||||
spiral_progress_bar.set_precision(4, 1)
|
||||
@@ -174,7 +174,7 @@ def test_bar_set_precision(spiral_progress_bar):
|
||||
assert ring_precision == [2, 4, 2]
|
||||
|
||||
spiral_progress_bar.set_value([10.1234, 20.1234, 30.1234])
|
||||
ring_values = [ring.value for ring in spiral_progress_bar.rings]
|
||||
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
|
||||
assert ring_values == [10.12, 20.1234, 30.12]
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ def test_set_min_max_value(spiral_progress_bar):
|
||||
assert ring_max_values == [10, 10]
|
||||
|
||||
spiral_progress_bar.set_value([5, 15])
|
||||
ring_values = [ring.value for ring in spiral_progress_bar.rings]
|
||||
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
|
||||
assert ring_values == [5, 10]
|
||||
|
||||
|
||||
|
||||
25
tests/unit_tests/test_stop_button.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets import StopButton
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stop_button(qtbot, mocked_client):
|
||||
widget = StopButton(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def test_stop_button(stop_button):
|
||||
assert stop_button.text() == "Stop"
|
||||
assert stop_button.styleSheet() == "background-color: #cc181e; color: white"
|
||||
stop_button.click()
|
||||
assert stop_button.queue.request_scan_abortion.called
|
||||
assert stop_button.queue.request_queue_reset.called
|
||||
stop_button.close()
|
||||
@@ -73,7 +73,7 @@ def test_create_waveform1D_by_config(bec_figure):
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"colormap": "plasma",
|
||||
"color_map_z": "plasma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
@@ -105,7 +105,7 @@ def test_create_waveform1D_by_config(bec_figure):
|
||||
"parent_id": "widget_1",
|
||||
"label": "curve-custom",
|
||||
"color": "blue",
|
||||
"colormap": "plasma",
|
||||
"color_map_z": "plasma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
@@ -360,7 +360,7 @@ def test_curve_add_by_config(bec_figure):
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"colormap": "plasma",
|
||||
"color_map_z": "plasma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
|
||||
27
tests/unit_tests/test_website_widget.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import pytest
|
||||
from qtpy.QtCore import QUrl
|
||||
|
||||
from bec_widgets.widgets.website.website import WebsiteWidget
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def website_widget(qtbot, mocked_client):
|
||||
widget = WebsiteWidget(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
widget.page().deleteLater()
|
||||
qtbot.wait(1000)
|
||||
|
||||
|
||||
def test_website_widget_set_url(website_widget):
|
||||
website_widget.set_url("https://scilog.psi.ch")
|
||||
assert website_widget.url() == QUrl("https://scilog.psi.ch")
|
||||
|
||||
website_widget.set_url(None)
|
||||
assert website_widget.url() == QUrl("https://scilog.psi.ch")
|
||||
|
||||
website_widget.set_url("https://google.com")
|
||||
assert website_widget.get_url() == "https://google.com"
|
||||