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

Compare commits

..

45 Commits

Author SHA1 Message Date
4e52550f67 fix: import pyqtRemoveInputHook only for pyqt distributions 2024-06-12 14:32:15 +02:00
74182bb142 fix: terminal I/O based on socket notification instead of thread, fix "unexpected end of data" 2024-06-12 12:15:59 +02:00
2b5068903a feat(widgets/console): BECConsole based on termqt added 2024-06-12 12:15:55 +02:00
semantic-release
718950cf0d 0.62.0
Automatically generated by python-semantic-release
2024-06-12 10:01:48 +00:00
17a0068757 doc: add documentation about creating custom GUI applications embedding BEC Widgets 2024-06-12 11:54:47 +02:00
abc6caa2d0 feat: implement non-polling, interruptible waiting of gui instruction response with timeout 2024-06-12 11:43:08 +02:00
semantic-release
99fb82561b 0.61.0
Automatically generated by python-semantic-release
2024-06-12 06:27:41 +00:00
61ba08d0b8 feat(widgets/stop_button): General stop button added 2024-06-12 01:11:06 +02:00
40b5688158 refactor: improve labe of auto_update script 2024-06-10 08:27:32 +02:00
semantic-release
0a4e253cbd 0.60.0
Automatically generated by python-semantic-release
2024-06-08 17:35:57 +00:00
6428e38ab9 fix: removed BECConnector from rpc client interface 2024-06-08 19:05:57 +02:00
fc4f4f81ad ci: added git fetch for target branch 2024-06-08 19:05:57 +02:00
f6629852eb test: added missing pylint statement to header 2024-06-08 19:05:57 +02:00
3adf6cfd58 refactor: minor cleanup 2024-06-08 19:05:57 +02:00
b15816ca9f refactor: disabled pylint for auto-gen client 2024-06-08 19:05:57 +02:00
6b1d5827d6 ci: fixed pylint-check 2024-06-08 19:05:57 +02:00
f0391f59c9 feat: added isort to bw-generate-cli 2024-06-08 19:05:57 +02:00
006a0894b8 fix: added bec_ipython_client as dependency; needed for jupyter widget 2024-06-08 19:05:57 +02:00
9c5a471234 refactor(isort): added bec_widgets as known first party package 2024-06-08 19:05:57 +02:00
1c7f4912ce feat: added entry point for bw-generate-cli 2024-06-08 19:05:57 +02:00
df1be10057 feat(cli): auto-discover rpc-enabled widgets 2024-06-08 19:05:57 +02:00
954c576131 fix(BECFigure): removed duplicated user access for plot 2024-06-08 19:05:57 +02:00
867720a897 fix(bec_connector): field validator should be a classmethod 2024-06-08 19:05:57 +02:00
2b40602bdc refactor(dock): parent_dock_area changed to orig_area (native for pyqtgraph) 2024-06-08 16:00:45 +02:00
11173b9c0a ci: cleanup 2024-06-08 08:54:08 +02:00
semantic-release
52d46e77db 0.59.1
Automatically generated by python-semantic-release
2024-06-07 23:24:10 +00:00
e7838b0f2f fix(curve): set_color_map_z typo fixed in user access 2024-06-08 01:17:13 +02:00
semantic-release
2ae3810cf6 0.59.0
Automatically generated by python-semantic-release
2024-06-07 20:47:10 +00:00
178fe4d2da ci: merged additional tests to parallel matrix job 2024-06-07 22:37:24 +02:00
2d79ef8fe5 ci: added webengine dependencies 2024-06-07 22:37:24 +02:00
d56c5493cd build: added webengine dependency 2024-06-07 22:37:24 +02:00
cf6e5a40fc docs: added website docs 2024-06-07 22:37:24 +02:00
64abd67b9b feat(widget): added simple website widget with rpc 2024-06-07 22:37:24 +02:00
semantic-release
c19e856800 0.58.1
Automatically generated by python-semantic-release
2024-06-07 19:39:16 +00:00
02a26086c4 fix(dock): new dock can be detached upon creation 2024-06-07 21:32:38 +02:00
semantic-release
35f880bc2f 0.58.0
Automatically generated by python-semantic-release
2024-06-07 19:25:17 +00:00
c0ddeceeea test(color): validation tests added 2024-06-07 19:16:58 +02:00
67fd5e8581 fix: bar colormap dynamic setting 2024-06-07 18:52:37 +02:00
bf699ec1fb fix: formatting isort 2024-06-07 18:40:42 +02:00
3094632134 feat(utils.colors): general color validators 2024-06-07 18:34:57 +02:00
6985ff0fce fix(curve): 2D scatter updated if color_map_z is changed 2024-06-07 16:28:51 +02:00
33f7be42c5 fix(curve): color_map_z setting works 2024-06-07 16:28:51 +02:00
semantic-release
36fac70361 0.57.7
Automatically generated by python-semantic-release
2024-06-07 14:15:17 +00:00
ca5e8d2fbb fix: add model_config to pydantic models to allow runtime checks after creation 2024-06-07 15:44:09 +02:00
828067f486 docs: added schema of BECDockArea and BECFigure 2024-06-06 20:14:47 +02:00
43 changed files with 2540 additions and 1708 deletions

View File

@@ -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

View File

@@ -2,6 +2,138 @@
## v0.62.0 (2024-06-12)
### Feature
* feat: implement non-polling, interruptible waiting of gui instruction response with timeout ([`abc6caa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3))
### Unknown
* doc: add documentation about creating custom GUI applications embedding BEC Widgets ([`17a0068`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/17a00687579f5efab1990cd83862ec0e78198633))
## v0.61.0 (2024-06-12)
### Feature
* feat(widgets/stop_button): General stop button added ([`61ba08d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61ba08d0b8df9f48f5c54c7c2b4e6d395206e7e6))
### Refactor
* refactor: improve labe of auto_update script ([`40b5688`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/40b568815893cd41af3531bb2e647ca1e2e315f4))
## v0.60.0 (2024-06-08)
### Ci
* ci: added git fetch for target branch ([`fc4f4f8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc4f4f81ad1be99cf5112f2188a46c5bed2679ee))
* ci: fixed pylint-check ([`6b1d582`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b1d5827d6599f06a3acd316060a8d25f0686d54))
* ci: cleanup ([`11173b9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/11173b9c0a7dc4b36e35962042e5b86407da49f1))
### Feature
* 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: 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: 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))
* refactor(isort): added bec_widgets as known first party package ([`9c5a471`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9c5a471234ed2928e4527b079436db2a807c5f6f))
* refactor(dock): parent_dock_area changed to orig_area (native for pyqtgraph) ([`2b40602`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2b40602bdc593ece0447ec926c2100414bd5cf67))
### Test
* test: added missing pylint statement to header ([`f662985`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f6629852ebc2b4ee239fa560cc310a5ae2627cf7))
## 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
@@ -31,129 +163,3 @@
### Fix
* fix(docks): set_title do update dock internal _name now ([`15cbc21`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/15cbc21e5bb3cf85f5822d44a2b3665b5aa2f346))
* fix(docks): docks widget_list adn dockarea panels return values fixed ([`ffae5ee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ffae5ee54e6b43da660131092452adff195ba4fb))
## v0.57.3 (2024-06-06)
### Documentation
* docs(bar): docs updated ([`4be0d14`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4be0d14b7445c2322c2aef86257db168a841265c))
* docs: fixed syntax of add_widget ([`a951ebf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a951ebf1be6c086d094aa8abef5e0dfd1b3b8558))
* docs: added auto update; closes #206 ([`32da803`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/32da803df9f7259842c43e85ba9a0ce29a266d06))
* docs: cleanup ([`07d60cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/07d60cf7355d2edadb3c5ef8b86607d74b360455))
### Fix
* fix(ring): automatic updates are disabled uf user specify updates manually with .set_update; 'scan_progres' do not reset number of rings ([`e883dba`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e883dbad814dbcc0a19c341041c6d836e58a5918))
* fix(ring): enable_auto_updates(True) do not reset properties of already setup bars ([`a2abad3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a2abad344f4c0039516eb60a825afb6822c5b19a))
* fix(ring): set_min_max accepts floats ([`d44b1cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d44b1cf8b107cf02deedd9154b77d01c7f9ed05d))
* fix(ring): set_update changed to Literals, no need to specify endpoint manually ([`c5b6499`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c5b6499e41eb1495bf260436ca3e1b036182c360))
## v0.57.2 (2024-06-06)
### Fix
* fix(test/e2e): autoupdate e2e rewritten ([`e1af5ca`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e1af5ca60f0616835f9f41d84412f29dc298c644))
* fix(test/e2e): spiral_progress_bar e2e tests rewritten to use config_dict ([`7fb31fc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7fb31fc4d762ff4ca839971b3092a084186f81b8))
* fix(test/e2e): dockarea and dock e2e tests changed to check asserts against config_dict ([`5c6ba65`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5c6ba65469863ea1e6fc5abdc742650e20eba9b9))
* fix: rpc_server_dock fixture now spawns the server process ([`cd9fc46`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cd9fc46ff8a947242c8c28adcd73d7de60b11c44))
* fix: accept scalars or numpy arrays of 1 element ([`2a88e17`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2a88e17b23436c55d25b7d3449e4af3a7689661c))
### Refactor
* refactor: move _get_output and _start_plot_process at the module level ([`69f4371`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/69f4371007c66aee6b7521a6803054025adf8c92))
## v0.57.1 (2024-06-06)
### Documentation
* docs: docs refactored from add_widget_bec to add_widget ([`c3f4845`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c3f4845b4f95005ff737fed5542600b0b9a9cc2b))
### Fix
* fix: tests references to add_widget_bec refactored ([`f51b25f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f51b25f0af4ab8b0a75ee48a40bfbb079c16e9d1))
* fix(dock): add_widget and add_widget_bec consolidated ([`8ae323f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ae323f5c3c0d9d0f202d31d5e8374a272a26be2))
## v0.57.0 (2024-06-05)
### Documentation
* docs: extend user documentation for BEC Widgets ([`4160f3d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4160f3d6d7ec1122785b5e3fdfc4afe67a95e9a1))
### Feature
* feat(widgets/console): BECJupyterConsole added ([`8c03034`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8c03034acf6b3ed1e346ebf1b785d41068513cc5))
## v0.56.3 (2024-06-05)
### Ci
* ci: increased verbosity for e2e tests ([`4af1abe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4af1abe4e15b62d2f7e70bf987a1a7d8694ef4d5))
### Fix
* fix: fixed support for auto updates ([`131f49d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/131f49da8ea65af4d44b50e81c1acfc29cd92093))
## v0.56.2 (2024-06-05)
### Documentation
* docs: restructured docs layout ([`3c9181d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3c9181d93d68faa4efb3b91c486ca9ca935975a0))
### Fix
* fix(bar): ring saves current value in config ([`9648e3e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9648e3ea96a4109be6be694d855151ed6d3ad661))
* fix(dock): dock saves configs of all children widgets ([`4be756a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4be756a8676421c3a3451458995232407295df84))
* fix(dock_area): save/restore state is saved in config ([`46face0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/46face0ee59122f04cb383da685a6658beeeb389))
* fix(figure): added correct types of configs to subplot widgets ([`6f3b1ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6f3b1ea985c18929b9bab54239eeb600f03b274a))
## v0.56.1 (2024-06-04)
### Fix
* fix(spiral_progress_bar/rings): config min/max values added check for floats ([`9d615c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d615c915c8f7cc2ea8f1dc17993b98fe462c682))
* fix(spiral_progress_bar): Endpoint is always stored as a string in the RingConnection Config ([`d253991`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2539918b296559e1d684344e179775a2423daa9))
## 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))
### Fix
* 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))

View File

@@ -1 +1 @@
from .client import BECDockArea, BECFigure
from .client import *

View File

@@ -15,6 +15,7 @@ class ScanInfo(BaseModel):
scan_report_devices: list
monitored_devices: list
status: str
model_config: dict = {"validate_assignment": True}
class AutoUpdates:
@@ -117,7 +118,7 @@ class AutoUpdates:
if not dev_y:
return
fig.clear_all()
plt = fig.plot(x_name=dev_x, y_name=dev_y)
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:
@@ -131,7 +132,9 @@ class AutoUpdates:
dev_y = info.scan_report_devices[1]
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}")
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:
@@ -146,5 +149,5 @@ class AutoUpdates:
if not dev_y:
return
fig.clear_all()
plt = fig.plot(x_name=dev_x, y_name=dev_y, label=f"Scan {info.scan_number}")
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)

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ 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
@@ -24,6 +24,8 @@ if TYPE_CHECKING:
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",))
@@ -205,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
@@ -231,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.
@@ -253,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:
@@ -285,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.

View File

@@ -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()

View File

@@ -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:

View File

@@ -86,9 +86,10 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.console_layout.addWidget(self.console)
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)
@@ -97,8 +98,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.w3 = self.figure[1, 0]
# curves for w1
self.w1.plot(x_name="samx", y_name="samy", z_name="bpm4i")
self.w1.plot(x_name="samx", y_name="samy", z_name="bpm3a")
self.c1 = self.w1.get_config()
def _init_dock(self):
@@ -137,17 +136,18 @@ if __name__ == "__main__": # pragma: no cover
module_path = os.path.dirname(bec_widgets.__file__)
app = QApplication(sys.argv)
app.setApplicationName("Jupyter Console")
app.setApplicationDisplayName("Jupyter Console")
qdarktheme.setup_theme("auto")
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()
app = QApplication(sys.argv)
app.setApplicationName("Jupyter Console")
app.setApplicationDisplayName("Jupyter Console")
# qdarktheme.setup_theme("auto")
icon = QIcon()
icon.addFile(os.path.join(module_path, "assets", "terminal_icon.png"), size=QSize(48, 48))
app.setWindowIcon(icon)
win = JupyterConsoleWindow()
win.show()

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +1,4 @@
from .buttons import StopButton
from .dock import BECDock, BECDockArea
from .figure import BECFigure, FigureConfig
from .scan_control import ScanControl

View File

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

View 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_())

View File

View 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())

View File

@@ -63,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)
@@ -73,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):
"""
@@ -129,7 +127,7 @@ 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
@@ -153,38 +151,6 @@ 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)
# self.config.widgets[widget.gui_id] = widget.config
#
# return widget
def add_widget(
self,
widget: BECConnector | str,
@@ -238,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):
"""
@@ -263,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):
"""

View File

@@ -137,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,
@@ -152,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.
@@ -192,6 +194,8 @@ class BECDockArea(BECConnector, DockArea):
if self._instructions_visible:
self._instructions_visible = False
self.update()
if floating:
dock.detach()
return dock
def detach_dock(self, dock_name: str) -> BECDock:

View File

@@ -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:

View File

@@ -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):

View File

@@ -53,7 +53,6 @@ class BECWaveform(BECPlotBase):
"set_y_lim",
"set_grid",
"lock_aspect_ratio",
"plot",
"remove",
]
scan_signal_update = pyqtSignal()
@@ -364,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,
@@ -396,7 +395,7 @@ class BECWaveform(BECPlotBase):
Returns:
BECCurve: The curve object.
"""
curve = BECCurve(config=config, name=name, parent_item=self.plot_item)
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
@@ -564,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

View File

@@ -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]:
"""

View File

@@ -13,6 +13,7 @@ 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):

View File

@@ -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."
)
@@ -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):
@@ -368,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):

View File

View 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())

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -5,6 +5,10 @@ In order to use BEC Widgets as a plotting tool for BEC, it needs to be [installe
## 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**
![BECDockArea.png](BECDockArea.png)
## 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -3,6 +3,10 @@
[`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**
![BECFigure.png](BECFigure.png)
(user.widgets.waveform_1d)=
## [1D Waveform Widget](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform)

View 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)
```

View 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/")
```

View File

@@ -10,6 +10,8 @@ hidden: false
bec_figure/
spiral_progress_bar/
website/
buttons/
```

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "0.57.6"
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"
]
@@ -37,14 +40,17 @@ dev = [
"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 = ["*"]
@@ -57,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"

View File

@@ -253,8 +253,14 @@ def test_auto_update(bec_client_lib, rpc_server_dock):
plt_data = widgets[0].get_all_data()
# check plotted data
assert plt_data["bpm4i-bpm4i"]["x"] == last_scan_data["samx"]["samx"].val
assert plt_data["bpm4i-bpm4i"]["y"] == last_scan_data["bpm4i"]["bpm4i"].val
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
@@ -268,5 +274,11 @@ def test_auto_update(bec_client_lib, rpc_server_dock):
last_scan_data = queue.scan_storage.storage[-1].data
# check plotted data
assert plt_data[f"Scan {status.scan.scan_number}"]["x"] == last_scan_data["samx"]["samx"].val
assert plt_data[f"Scan {status.scan.scan_number}"]["y"] == last_scan_data["samy"]["samy"].val
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
)

View 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
)

View File

@@ -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

View 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()

View File

@@ -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,

View 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"