mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-09 10:10:55 +02:00
Compare commits
44 Commits
feat/dm_ma
...
v2.40.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13299aeeb3 | ||
| d681ba538b | |||
| 2bf489600e | |||
| 7e88a002b6 | |||
| 20a59af648 | |||
| 540cfc37be | |||
| e59f27a22d | |||
| df8065ea40 | |||
| 2f3dc2ce6b | |||
| a006f95f21 | |||
| 8111a4a21b | |||
| 962ab774e6 | |||
| 2f798be7b0 | |||
| 5a5d32312b | |||
| 0844a9e119 | |||
| db7dd4f8d4 | |||
| f083dff612 | |||
| 4be70580a6 | |||
| d19001c94e | |||
| f25f86522f | |||
|
|
948283bc13 | ||
| 50696bce4c | |||
|
|
1d988a4c57 | ||
| 565c0bd1e7 | |||
| 975404f483 | |||
|
|
165e5e7d84 | ||
| 108ddae6ca | |||
|
|
9737acad58 | ||
| 65bc5f5421 | |||
| 475ca9f2d8 | |||
| bbb5fc6ce1 | |||
| b1b6c5e6a5 | |||
| 3e339348dd | |||
|
|
4f075151d5 | ||
| 0a24ac2c40 | |||
| 3a2ec9f1b7 | |||
| 4dc4ede1d2 | |||
| 556832fd48 | |||
| 72b6f74252 | |||
| b703b37bbd | |||
| 18ef35f22a | |||
| fe67a4f325 | |||
|
|
f1c3d77a45 | ||
| ad7cdc60dd |
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
2
.github/workflows/pytest-matrix.yml
vendored
2
.github/workflows/pytest-matrix.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
env:
|
||||
BEC_WIDGETS_BRANCH: main # Set the branch you want for bec_widgets
|
||||
|
||||
4
.github/workflows/stale-issues.yml
vendored
4
.github/workflows/stale-issues.yml
vendored
@@ -2,10 +2,14 @@ name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '00 10 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
|
||||
289
.gitlab-ci.yml
289
.gitlab-ci.yml
@@ -1,289 +0,0 @@
|
||||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official language image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/python/tags/
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
|
||||
#commands to run in the Docker container before starting each job.
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
BEC_CORE_BRANCH:
|
||||
description: bec branch
|
||||
value: main
|
||||
OPHYD_DEVICES_BRANCH:
|
||||
description: ophyd_devices branch
|
||||
value: main
|
||||
CHILD_PIPELINE_BRANCH: $CI_DEFAULT_BRANCH
|
||||
CHECK_PKG_VERSIONS:
|
||||
description: Whether to run additional tests against min/max/random selection of dependencies. Set to 1 for running.
|
||||
value: 0
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
- if: $CI_PIPELINE_SOURCE == "pipeline"
|
||||
- if: $CI_PIPELINE_SOURCE == "parent_pipeline"
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
include:
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
- project: "bec/awi_utils"
|
||||
file: "/templates/check-packages-job.yml"
|
||||
inputs:
|
||||
stage: test
|
||||
path: "."
|
||||
pytest_args: "-v,--random-order,tests/unit_tests"
|
||||
pip_args: ".[dev]"
|
||||
|
||||
# different stages in the pipeline
|
||||
stages:
|
||||
- Formatter
|
||||
- test
|
||||
- AdditionalTests
|
||||
- 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
|
||||
- echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
|
||||
.install-repos: &install-repos
|
||||
- pip install -e ./ophyd_devices
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e ./bec/bec_ipython_client
|
||||
- pip install -e ./bec/pytest_bec_e2e
|
||||
|
||||
.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 xvfb
|
||||
- *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";
|
||||
test -d bec_widgets || git clone --branch $CHILD_PIPELINE_BRANCH https://gitlab.psi.ch/bec/bec_widgets.git; cd bec_widgets;
|
||||
fi
|
||||
|
||||
formatter:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
script:
|
||||
- pip install -e ./[dev]
|
||||
- isort --check --diff --line-length=100 --profile=black --multi-line=3 --trailing-comma ./
|
||||
- black --check --diff --color --line-length=100 --skip-magic-trailing-comma ./
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
|
||||
|
||||
pylint:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
before_script:
|
||||
- pip install pylint pylint-exit anybadge
|
||||
- pip install -e .[dev]
|
||||
script:
|
||||
- mkdir ./pylint
|
||||
- pylint ./bec_widgets --output-format=text --output=./pylint/pylint.log | tee ./pylint/pylint.log || pylint-exit $?
|
||||
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log)
|
||||
- anybadge --label=Pylint --file=pylint/pylint.svg --value=$PYLINT_SCORE 2=red 4=orange 8=yellow 10=green
|
||||
- echo "Pylint score is $PYLINT_SCORE"
|
||||
artifacts:
|
||||
paths:
|
||||
- ./pylint/
|
||||
expire_in: 1 week
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
|
||||
|
||||
pylint-check:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
allow_failure: true
|
||||
before_script:
|
||||
- pip install pylint pylint-exit anybadge
|
||||
- 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 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_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"
|
||||
|
||||
# Fail the job if the pylint score is below 9
|
||||
- if [ "$(echo "$PYLINT_SCORE < 9" | bc)" -eq 1 ]; then echo "Your pylint score is below the acceptable threshold (9)."; exit 1; fi
|
||||
artifacts:
|
||||
paths:
|
||||
- ./pylint/
|
||||
expire_in: 1 week
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
|
||||
|
||||
tests:
|
||||
stage: test
|
||||
needs: []
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- *install-repos
|
||||
- pip install -e .[dev,pyside6]
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --maxfail=2 --random-order --full-trace ./tests/unit_tests
|
||||
- coverage report
|
||||
- coverage xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
artifacts:
|
||||
reports:
|
||||
junit: report.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
paths:
|
||||
- tests/reference_failures/
|
||||
when: always
|
||||
|
||||
generate-client-check:
|
||||
stage: test
|
||||
needs: []
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- *install-repos
|
||||
- pip install -e .[dev,pyside6]
|
||||
- bw-generate-cli --target bec_widgets
|
||||
# if there are changes in the generated files, fail the job
|
||||
- git diff --exit-code
|
||||
|
||||
test-matrix:
|
||||
parallel:
|
||||
matrix:
|
||||
- PYTHON_VERSION:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
QT_PCKG:
|
||||
- "pyside6"
|
||||
|
||||
stage: AdditionalTests
|
||||
needs: []
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
PYTHON_VERSION: ""
|
||||
QT_PCKG: ""
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:$PYTHON_VERSION
|
||||
script:
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- *install-repos
|
||||
- pip install -e .[dev,$QT_PCKG]
|
||||
- pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
|
||||
end-2-end-conda:
|
||||
stage: End2End
|
||||
needs: []
|
||||
image: continuumio/miniconda3:25.1.1-2
|
||||
allow_failure: false
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- conda config --show-sources
|
||||
- conda config --add channels conda-forge
|
||||
- conda config --system --remove channels https://repo.anaconda.com/pkgs/main
|
||||
- conda config --system --remove channels https://repo.anaconda.com/pkgs/r
|
||||
- conda config --remove channels https://repo.anaconda.com/pkgs/main
|
||||
- conda config --remove channels https://repo.anaconda.com/pkgs/r
|
||||
- conda config --show-sources
|
||||
- conda config --set channel_priority strict
|
||||
- conda config --set always_yes yes --set changeps1 no
|
||||
- conda create -q -n test-environment python=3.11
|
||||
- conda init bash
|
||||
- source ~/.bashrc
|
||||
- conda activate test-environment
|
||||
|
||||
- cd ./bec
|
||||
- source ./bin/install_bec_dev.sh -t
|
||||
- cd ../
|
||||
- pip install -e ./ophyd_devices
|
||||
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pytest -v --files-path ./ --start-servers --random-order ./tests/end-2-end
|
||||
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- ./logs/*.log
|
||||
expire_in: 1 week
|
||||
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "web"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "pipeline"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "parent_pipeline"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "production"'
|
||||
- if: "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^pre_release.*$/"
|
||||
|
||||
semver:
|
||||
stage: Deploy
|
||||
needs: ["tests"]
|
||||
script:
|
||||
- git config --global user.name "ci_update_bot"
|
||||
- git config --global user.email "ci_update_bot@bec.ch"
|
||||
- git checkout "$CI_COMMIT_REF_NAME"
|
||||
- git reset --hard origin/"$CI_COMMIT_REF_NAME"
|
||||
|
||||
# delete all local tags
|
||||
- git tag -l | xargs git tag -d
|
||||
- git fetch --tags
|
||||
- git tag
|
||||
|
||||
# build and publish package
|
||||
- pip install python-semantic-release==9.* wheel build twine
|
||||
- export GL_TOKEN=$CI_UPDATES
|
||||
- semantic-release -vv version
|
||||
|
||||
# check if any artifacts were created
|
||||
- if [ ! -d dist ]; then echo No release will be made; exit 0; fi
|
||||
- twine upload dist/* -u __token__ -p $CI_PYPI_TOKEN --skip-existing
|
||||
- semantic-release publish
|
||||
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME == "main" && $CI_PROJECT_PATH == "bec/bec_widgets"'
|
||||
|
||||
pages:
|
||||
stage: Deploy
|
||||
needs: ["semver"]
|
||||
variables:
|
||||
TARGET_BRANCH: $CI_COMMIT_REF_NAME
|
||||
rules:
|
||||
- if: "$CI_COMMIT_TAG != null"
|
||||
variables:
|
||||
TARGET_BRANCH: $CI_COMMIT_TAG
|
||||
- if: '$CI_COMMIT_REF_NAME == "main" && $CI_PROJECT_PATH == "bec/bec_widgets"'
|
||||
script:
|
||||
- curl -X POST -d "branches=$CI_COMMIT_REF_NAME" -d "token=$RTD_TOKEN" https://readthedocs.org/api/v2/webhook/bec-widgets/253243/
|
||||
@@ -52,7 +52,7 @@ persistent=yes
|
||||
|
||||
# Minimum Python version to use for version dependent checks. Will default to
|
||||
# the version used to run pylint.
|
||||
py-version=3.10
|
||||
py-version=3.11
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
|
||||
165
CHANGELOG.md
165
CHANGELOG.md
@@ -1,6 +1,171 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v2.40.0 (2025-10-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **curve_tree**: Fetching scan numbers directly from the bec client
|
||||
([`8111a4a`](https://github.com/bec-project/bec_widgets/commit/8111a4a21b7c1bd75316e9a1f1166b88ea52326d))
|
||||
|
||||
- **curve_tree**: Safeguard fetching scan numbers from BEC client
|
||||
([`df8065e`](https://github.com/bec-project/bec_widgets/commit/df8065ea4000b24235520756515aa18f812bb390))
|
||||
|
||||
- **curve_tree**: Scans are always fetched by scan ids
|
||||
([`20a59af`](https://github.com/bec-project/bec_widgets/commit/20a59af648a9808057df2226a3a3c12893cc5059))
|
||||
|
||||
- **waveform**: Cleanup of scan_history dialog if not closed manually before widget
|
||||
([`d681ba5`](https://github.com/bec-project/bec_widgets/commit/d681ba538be9ccec45a1ebd412cbc33c8c7c0ae2))
|
||||
|
||||
- **waveform**: Fetching scan number is not done from list but from .get_by_scan_number
|
||||
([`962ab77`](https://github.com/bec-project/bec_widgets/commit/962ab774e6afc73a321a5680e2862d9e41812888))
|
||||
|
||||
- **waveform**: If scan id and scan number is provided, the scan is fetched from the scan id
|
||||
([`e59f27a`](https://github.com/bec-project/bec_widgets/commit/e59f27a22de490768c814c80642a7a91bebfef5b))
|
||||
|
||||
- **waveform**: Safeguard added to the fetching history data
|
||||
([`540cfc3`](https://github.com/bec-project/bec_widgets/commit/540cfc37be65afcf721773564adc85de681a9d07))
|
||||
|
||||
- **waveform**: Safeguard for _scan_history_closed
|
||||
([`2bf4896`](https://github.com/bec-project/bec_widgets/commit/2bf489600e96bb5b47d89bed261614f62c970ca9))
|
||||
|
||||
- **waveform**: Safeguard for if scan_item is a list
|
||||
([`7e88a00`](https://github.com/bec-project/bec_widgets/commit/7e88a002b6ca40fc85fde993282b8706f140d9aa))
|
||||
|
||||
- **waveform**: Update x suffix label with x property change, do not wait for next update cycle
|
||||
([`d19001c`](https://github.com/bec-project/bec_widgets/commit/d19001c94e652c0c3e18f8d7903fd1ccff1111cd))
|
||||
|
||||
- **waveform**: X_data checked with is scalar instead of len()
|
||||
([`db7dd4f`](https://github.com/bec-project/bec_widgets/commit/db7dd4f8d4b1210e65c852f6193fc8cf0f4809a5))
|
||||
|
||||
### Build System
|
||||
|
||||
- **bec_lib**: Bec_lib dependency raised to 3.68
|
||||
([`2f3dc2c`](https://github.com/bec-project/bec_widgets/commit/2f3dc2ce6b7133fc5582bd6996a674590cf1002d))
|
||||
|
||||
### Chores
|
||||
|
||||
- Add dependabot config
|
||||
([`f25f865`](https://github.com/bec-project/bec_widgets/commit/f25f86522f0a2e9dd24ca862ea8de89873951f83))
|
||||
|
||||
### Features
|
||||
|
||||
- **waveform**: New type of curve - history curve
|
||||
([`f083dff`](https://github.com/bec-project/bec_widgets/commit/f083dff6128c6256443b49f54ab12b54f1b90d66))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **test_waveform**: Test waveform renamed
|
||||
([`2f798be`](https://github.com/bec-project/bec_widgets/commit/2f798be7b0d43d304ccbd0e992a9d62f1aa1dd5f))
|
||||
|
||||
- **waveform**: Separate method to fetch scan item from history
|
||||
([`4be7058`](https://github.com/bec-project/bec_widgets/commit/4be70580a60293204b135c6ea77978f1dcf8aa5f))
|
||||
|
||||
### Testing
|
||||
|
||||
- **conftest**: Suppress_message_box for error popups fixture autouse True
|
||||
([`0844a9e`](https://github.com/bec-project/bec_widgets/commit/0844a9e11975a34780b1dc413f5145517d1a1a22))
|
||||
|
||||
- **plotting_framework_e2e**: Fetching history curve
|
||||
([`a006f95`](https://github.com/bec-project/bec_widgets/commit/a006f95f211ad115019967e365a6627d9678a1e3))
|
||||
|
||||
- **waveform,curve_tree**: Test extended to cover history curve behaviour
|
||||
([`5a5d323`](https://github.com/bec-project/bec_widgets/commit/5a5d32312b08e1edeb69243daddfaaa9bac22273))
|
||||
|
||||
|
||||
## v2.39.1 (2025-10-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Explicitly pass the cached readout flag
|
||||
([`50696bc`](https://github.com/bec-project/bec_widgets/commit/50696bce4ce14c61b4bdda8c6fb40967972e6b23))
|
||||
|
||||
|
||||
## v2.39.0 (2025-09-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **rpc**: Fix hide/show
|
||||
([`975404f`](https://github.com/bec-project/bec_widgets/commit/975404f483ddae041d9f4d819f39c53cec191439))
|
||||
|
||||
### Features
|
||||
|
||||
- **rpc_base**: Windows can be raised to front from CLI
|
||||
([`565c0bd`](https://github.com/bec-project/bec_widgets/commit/565c0bd1e7f4684d8401b6a2827c35422b1125c4))
|
||||
|
||||
|
||||
## v2.38.4 (2025-09-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **image**: Add support for specifying preview signals through cli
|
||||
([`108ddae`](https://github.com/bec-project/bec_widgets/commit/108ddae6ca3501a57b499c7080a36cf41a653074))
|
||||
|
||||
|
||||
## v2.38.3 (2025-09-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector**: Only flush pending events
|
||||
([`475ca9f`](https://github.com/bec-project/bec_widgets/commit/475ca9f2d81bcc2bb0c7b104c0712b13d6616c08))
|
||||
|
||||
- **ringprogressbar**: Fix client signature
|
||||
([`65bc5f5`](https://github.com/bec-project/bec_widgets/commit/65bc5f5421077da70ef5068d51e36119e1055955))
|
||||
|
||||
- **ringprogressbar**: Various fixes and improvements
|
||||
([`bbb5fc6`](https://github.com/bec-project/bec_widgets/commit/bbb5fc6ce17248a948c6fd4a7652d17d64a79d2a))
|
||||
|
||||
### Chores
|
||||
|
||||
- Deprecate 3.10, add 3.13
|
||||
([`3e33934`](https://github.com/bec-project/bec_widgets/commit/3e339348dd3d0a3b12522312132fca139dc22835))
|
||||
|
||||
### Testing
|
||||
|
||||
- **ringprogressbar**: Extend e2e test
|
||||
([`b1b6c5e`](https://github.com/bec-project/bec_widgets/commit/b1b6c5e6a5dd81965baa5c742e9bdae8cdb4f09b))
|
||||
|
||||
|
||||
## v2.38.2 (2025-09-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **crosshair**: Ignore fetching data and markers from invisible items
|
||||
([`72b6f74`](https://github.com/bec-project/bec_widgets/commit/72b6f74252e1f36339945c549049b166cccf3561))
|
||||
|
||||
- **plot_base**: Crosshair items are excluded from visible curves and from auto_range
|
||||
([`4dc4ede`](https://github.com/bec-project/bec_widgets/commit/4dc4ede1d251d081e5bcf3d37fcc784982c9258e))
|
||||
|
||||
- **plot_base**: Visible items injected into plot item
|
||||
([`b703b37`](https://github.com/bec-project/bec_widgets/commit/b703b37bbdbf97182b58ac4c69c1384fa78d0c12))
|
||||
|
||||
- **waveform**: Changing curve visibility refresh markers
|
||||
([`556832f`](https://github.com/bec-project/bec_widgets/commit/556832fd48bcb16b95df8cf91417d7045bbca2a3))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Fix stale issues job permissions; add workflow dispatch option
|
||||
([`fe67a4f`](https://github.com/bec-project/bec_widgets/commit/fe67a4f325cbd41f13102e5698d86ed9e90b048e))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Move to autoapi
|
||||
([`18ef35f`](https://github.com/bec-project/bec_widgets/commit/18ef35f22a1b7496b13f833e63a4f3875e1497e3))
|
||||
|
||||
### Testing
|
||||
|
||||
- **crosshair**: Visibility test added with plotbase fixture
|
||||
([`3a2ec9f`](https://github.com/bec-project/bec_widgets/commit/3a2ec9f1b74c4bb5f239940b874576a877ce45c0))
|
||||
|
||||
|
||||
## v2.38.1 (2025-08-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Move thefuzz dependency to prod
|
||||
([`ad7cdc6`](https://github.com/bec-project/bec_widgets/commit/ad7cdc60dd6da6c5291f8b42932aacb12aa671a6))
|
||||
|
||||
|
||||
## v2.38.0 (2025-08-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[](https://pypi.org/project/bec-widgets/)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://www.python.org)
|
||||
[](https://www.python.org)
|
||||
[](https://doc.qt.io/qtforpython/)
|
||||
[](https://conventionalcommits.org)
|
||||
[](https://codecov.io/gh/bec-project/bec_widgets)
|
||||
|
||||
@@ -2218,7 +2218,7 @@ class Image(RPCBase):
|
||||
Set the image source and update the image.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to use for the image.
|
||||
monitor(str|tuple|None): The name of the monitor to use for the image, or a tuple of (device, signal) for preview signals. If None or empty string, the current monitor will be disconnected.
|
||||
monitor_type(str): The type of monitor to use. Options are "1d", "2d", or "auto".
|
||||
color_map(str): The color map to use for the image.
|
||||
color_bar(str): The type of color bar to use. Options are "simple" or "full".
|
||||
@@ -3880,7 +3880,7 @@ class RingProgressBar(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_precision(self, precision: "int", bar_index: "int" = None):
|
||||
def set_precision(self, precision: "int", bar_index: "int | None" = None):
|
||||
"""
|
||||
Set the precision for the progress bars. If bar_index is not provide, the precision will be set for all progress bars.
|
||||
|
||||
@@ -5079,6 +5079,8 @@ class Waveform(RPCBase):
|
||||
color: "str | None" = None,
|
||||
label: "str | None" = None,
|
||||
dap: "str | None" = None,
|
||||
scan_id: "str | None" = None,
|
||||
scan_number: "int | None" = None,
|
||||
**kwargs,
|
||||
) -> "Curve":
|
||||
"""
|
||||
@@ -5101,6 +5103,10 @@ class Waveform(RPCBase):
|
||||
dap(str): The dap model to use for the curve, only available for sync devices.
|
||||
If not specified, none will be added.
|
||||
Use the same string as is the name of the LMFit model.
|
||||
scan_id(str): Optional scan ID. When provided, the curve is treated as a **history** curve and
|
||||
the y‑data (and optional x‑data) are fetched from that historical scan. Such curves are
|
||||
never cleared by live‑scan resets.
|
||||
scan_number(int): Optional scan index. When provided, the curve is treated as a **history** curve and
|
||||
|
||||
Returns:
|
||||
Curve: The curve object.
|
||||
@@ -5143,11 +5149,11 @@ class Waveform(RPCBase):
|
||||
def update_with_scan_history(self, scan_index: "int" = None, scan_id: "str" = None):
|
||||
"""
|
||||
Update the scan curves with the data from the scan storage.
|
||||
Provide only one of scan_id or scan_index.
|
||||
If both arguments are provided, scan_id takes precedence and scan_index is ignored.
|
||||
|
||||
Args:
|
||||
scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
|
||||
scan_index(int, optional): Index of the scan to be updated. Defaults to None.
|
||||
scan_index(int, optional): Index (scan number) of the scan to be updated. Defaults to None.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
|
||||
@@ -285,6 +285,18 @@ class BECGuiClient(RPCBase):
|
||||
"""Hide the GUI window."""
|
||||
return self._hide_all()
|
||||
|
||||
def raise_window(self, wait: bool = True) -> None:
|
||||
"""
|
||||
Bring GUI windows to the front.
|
||||
If the GUI server is not running, it will be started.
|
||||
|
||||
Args:
|
||||
wait(bool): Whether to wait for the server to start. Defaults to True.
|
||||
"""
|
||||
if self._check_if_server_is_alive():
|
||||
return self._raise_all()
|
||||
return self._start(wait=wait)
|
||||
|
||||
def new(
|
||||
self,
|
||||
name: str | None = None,
|
||||
@@ -443,8 +455,8 @@ class BECGuiClient(RPCBase):
|
||||
self._update_dynamic_namespace(self._server_registry)
|
||||
|
||||
def _do_show_all(self):
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
||||
rpc_client._run_rpc("show") # pylint: disable=protected-access
|
||||
if self.launcher and len(self._top_level) == 0:
|
||||
self.launcher._run_rpc("show") # pylint: disable=protected-access
|
||||
for window in self._top_level.values():
|
||||
window.show()
|
||||
|
||||
@@ -454,11 +466,24 @@ class BECGuiClient(RPCBase):
|
||||
|
||||
def _hide_all(self):
|
||||
with wait_for_server(self):
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
||||
rpc_client._run_rpc("hide") # pylint: disable=protected-access
|
||||
if not self._killed:
|
||||
for window in self._top_level.values():
|
||||
window.hide()
|
||||
if self._killed:
|
||||
return
|
||||
self.launcher._run_rpc("hide")
|
||||
for window in self._top_level.values():
|
||||
window.hide()
|
||||
|
||||
def _do_raise_all(self):
|
||||
"""Bring GUI windows to the front."""
|
||||
if self.launcher and len(self._top_level) == 0:
|
||||
self.launcher._run_rpc("raise") # pylint: disable=protected-access
|
||||
for window in self._top_level.values():
|
||||
window._run_rpc("raise") # type: ignore[attr-defined]
|
||||
|
||||
def _raise_all(self):
|
||||
with wait_for_server(self):
|
||||
if self._killed:
|
||||
return
|
||||
return self._do_raise_all()
|
||||
|
||||
def _update_dynamic_namespace(self, server_registry: dict):
|
||||
"""
|
||||
|
||||
@@ -202,6 +202,11 @@ class RPCBase:
|
||||
parent = parent._parent
|
||||
return parent # type: ignore
|
||||
|
||||
def raise_window(self):
|
||||
"""Bring this widget (or its container) to the front."""
|
||||
# Use explicit call to ensure action name is 'raise' (not 'raise_')
|
||||
return self._run_rpc("raise")
|
||||
|
||||
def _run_rpc(
|
||||
self,
|
||||
method,
|
||||
@@ -225,6 +230,12 @@ class RPCBase:
|
||||
Returns:
|
||||
The result of the RPC call.
|
||||
"""
|
||||
if method in ["show", "hide", "raise"] and gui_id is None:
|
||||
obj = self._root._server_registry.get(self._gui_id)
|
||||
if obj is None:
|
||||
raise ValueError(f"Widget {self._gui_id} not found.")
|
||||
gui_id = obj.get("container_proxy") # type: ignore
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
rpc_msg = messages.GUIInstructionMessage(
|
||||
action=method,
|
||||
|
||||
@@ -55,7 +55,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
# "btn6": self.btn6,
|
||||
# "pb": self.pb,
|
||||
# "pi": self.pi,
|
||||
# "wf": self.wf,
|
||||
"wf": self.wf,
|
||||
# "scatter": self.scatter,
|
||||
# "scatter_mi": self.scatter,
|
||||
# "mwf": self.mwf,
|
||||
@@ -105,12 +105,11 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
# self.btn5 = QPushButton("Button 5")
|
||||
# self.btn6 = QPushButton("Button 6")
|
||||
#
|
||||
# fifth_tab = QWidget()
|
||||
# fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
# self.wf = Waveform()
|
||||
# fifth_tab_layout.addWidget(self.wf)
|
||||
# tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
# tab_widget.setCurrentIndex(4)
|
||||
fifth_tab = QWidget()
|
||||
fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
self.wf = Waveform()
|
||||
fifth_tab_layout.addWidget(self.wf)
|
||||
tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
#
|
||||
sixth_tab = QWidget()
|
||||
sixth_tab_layout = QVBoxLayout(sixth_tab)
|
||||
|
||||
@@ -173,7 +173,7 @@ class FakePositioner(BECPositioner):
|
||||
def set_read_value(self, value):
|
||||
self.read_value = value
|
||||
|
||||
def read(self):
|
||||
def read(self, cached=False):
|
||||
return self.signals
|
||||
|
||||
def set_limits(self, limits):
|
||||
|
||||
@@ -213,7 +213,7 @@ class BECConnector:
|
||||
- If there's a nearest BECConnector parent, only compare with children of that parent.
|
||||
- If parent is None (i.e., top-level object), compare with all other top-level BECConnectors.
|
||||
"""
|
||||
QApplication.processEvents()
|
||||
QApplication.sendPostedEvents()
|
||||
parent_bec = WidgetHierarchy._get_becwidget_ancestor(self)
|
||||
|
||||
if parent_bec:
|
||||
|
||||
@@ -209,8 +209,11 @@ class Crosshair(QObject):
|
||||
if self.highlighted_curve_index is not None and hasattr(self.plot_item, "visible_curves"):
|
||||
# Focus on the highlighted curve only
|
||||
self.items = [self.plot_item.visible_curves[self.highlighted_curve_index]]
|
||||
else:
|
||||
# Handle all curves
|
||||
elif hasattr(self.plot_item, "visible_items"): # PlotBase general case
|
||||
# Handle visible items in the plot item
|
||||
self.items = self.plot_item.visible_items()
|
||||
else: # Non PlotBase case
|
||||
# Handle all items
|
||||
self.items = self.plot_item.items
|
||||
|
||||
# Create or update markers
|
||||
|
||||
@@ -4,7 +4,17 @@ import typing
|
||||
from abc import abstractmethod
|
||||
from decimal import Decimal
|
||||
from types import GenericAlias, UnionType
|
||||
from typing import Callable, Final, Iterable, Literal, NamedTuple, OrderedDict, get_args
|
||||
from typing import (
|
||||
Callable,
|
||||
Final,
|
||||
Generic,
|
||||
Iterable,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
OrderedDict,
|
||||
TypeVar,
|
||||
get_args,
|
||||
)
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
@@ -350,11 +360,13 @@ class DictFormItem(DynamicFormItem):
|
||||
self._main_widget.replace_data(value)
|
||||
|
||||
|
||||
class _ItemAndWidgetType(NamedTuple):
|
||||
# TODO: this should be generic but not supported in 3.10
|
||||
item: type[int | float | str]
|
||||
_IW = TypeVar("_IW", bound=int | float | str)
|
||||
|
||||
|
||||
class _ItemAndWidgetType(NamedTuple, Generic[_IW]):
|
||||
item: type[_IW]
|
||||
widget: type[QWidget]
|
||||
default: int | float | str
|
||||
default: _IW
|
||||
|
||||
|
||||
class ListFormItem(DynamicFormItem):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import time
|
||||
import traceback
|
||||
import types
|
||||
from contextlib import contextmanager
|
||||
@@ -229,6 +230,8 @@ class RPCServer:
|
||||
if wait:
|
||||
while not self.rpc_register.object_is_registered(connector):
|
||||
QApplication.processEvents()
|
||||
logger.info(f"Waiting for {connector} to be registered...")
|
||||
time.sleep(0.1)
|
||||
|
||||
widget_class = getattr(connector, "rpc_widget_class", None)
|
||||
if not widget_class:
|
||||
|
||||
@@ -88,7 +88,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
||||
if not self._check_device_is_valid(device):
|
||||
return
|
||||
|
||||
data = self.dev[device].read()
|
||||
data = self.dev[device].read(cached=True)
|
||||
self._on_device_readback(
|
||||
device,
|
||||
self._device_ui_components(device),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Literal
|
||||
from typing import Literal, Sequence
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
@@ -307,7 +307,7 @@ class Image(ImageBase):
|
||||
Set the image source and update the image.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to use for the image.
|
||||
monitor(str|tuple|None): The name of the monitor to use for the image, or a tuple of (device, signal) for preview signals. If None or empty string, the current monitor will be disconnected.
|
||||
monitor_type(str): The type of monitor to use. Options are "1d", "2d", or "auto".
|
||||
color_map(str): The color map to use for the image.
|
||||
color_bar(str): The type of color bar to use. Options are "simple" or "full".
|
||||
@@ -322,10 +322,13 @@ class Image(ImageBase):
|
||||
if monitor is None or monitor == "":
|
||||
logger.warning(f"No monitor specified, cannot set image, old monitor is unsubscribed")
|
||||
return None
|
||||
if isinstance(monitor, tuple):
|
||||
|
||||
if isinstance(monitor, str):
|
||||
self.entry_validator.validate_monitor(monitor)
|
||||
elif isinstance(monitor, Sequence):
|
||||
self.entry_validator.validate_monitor(monitor[0])
|
||||
else:
|
||||
self.entry_validator.validate_monitor(monitor)
|
||||
raise ValueError(f"Invalid monitor type: {type(monitor)}")
|
||||
|
||||
self.set_image_update(monitor=monitor, type=monitor_type)
|
||||
if color_map is not None:
|
||||
@@ -347,7 +350,7 @@ class Image(ImageBase):
|
||||
if config.monitor is not None:
|
||||
for combo in (self.device_combo_box, self.dim_combo_box):
|
||||
combo.blockSignals(True)
|
||||
if isinstance(config.monitor, tuple):
|
||||
if isinstance(config.monitor, (list, tuple)):
|
||||
self.device_combo_box.setCurrentText(f"{config.monitor[0]}_{config.monitor[1]}")
|
||||
else:
|
||||
self.device_combo_box.setCurrentText(config.monitor)
|
||||
@@ -452,7 +455,7 @@ class Image(ImageBase):
|
||||
"""
|
||||
|
||||
# TODO consider moving connecting and disconnecting logic to Image itself if multiple images
|
||||
if isinstance(monitor, tuple):
|
||||
if isinstance(monitor, (list, tuple)):
|
||||
device = self.dev[monitor[0]]
|
||||
signal = monitor[1]
|
||||
if len(monitor) == 3:
|
||||
@@ -520,7 +523,7 @@ class Image(ImageBase):
|
||||
Args:
|
||||
monitor(str|tuple): The name of the monitor to disconnect, or a tuple of (device, signal) for preview signals.
|
||||
"""
|
||||
if isinstance(monitor, tuple):
|
||||
if isinstance(monitor, (list, tuple)):
|
||||
if self.subscriptions["main"].source == "device_monitor_1d":
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update_1d, MessageEndpoints.device_preview(monitor[0], monitor[1])
|
||||
|
||||
@@ -765,7 +765,7 @@ class MotorMap(PlotBase):
|
||||
float: Motor initial position.
|
||||
"""
|
||||
entry = self.entry_validator.validate_signal(name, None)
|
||||
init_position = round(float(self.dev[name].read()[entry]["value"]), precision)
|
||||
init_position = round(float(self.dev[name].read(cached=True)[entry]["value"]), precision)
|
||||
return init_position
|
||||
|
||||
def _sync_motor_map_selection_toolbar(self):
|
||||
|
||||
@@ -109,6 +109,7 @@ class PlotBase(BECWidget, QWidget):
|
||||
self.plot_widget.ci.setContentsMargins(0, 0, 0, 0)
|
||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
||||
self.plot_widget.addItem(self.plot_item)
|
||||
self.plot_item.visible_items = lambda: self.visible_items
|
||||
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
||||
|
||||
# PlotItem Addons
|
||||
@@ -893,15 +894,20 @@ class PlotBase(BECWidget, QWidget):
|
||||
return
|
||||
self._apply_autorange_only_visible_curves()
|
||||
|
||||
def _fetch_visible_curves(self):
|
||||
"""
|
||||
Fetch all visible curves from the plot item.
|
||||
"""
|
||||
visible_curves = []
|
||||
for curve in self.plot_item.curves:
|
||||
if curve.isVisible():
|
||||
visible_curves.append(curve)
|
||||
return visible_curves
|
||||
@property
|
||||
def visible_items(self):
|
||||
crosshair_items = []
|
||||
if self.crosshair:
|
||||
crosshair_items = [
|
||||
self.crosshair.v_line,
|
||||
self.crosshair.h_line,
|
||||
self.crosshair.coord_label,
|
||||
]
|
||||
return [
|
||||
item
|
||||
for item in self.plot_item.items
|
||||
if item.isVisible() and item not in crosshair_items
|
||||
]
|
||||
|
||||
def _apply_autorange_only_visible_curves(self):
|
||||
"""
|
||||
@@ -910,8 +916,9 @@ class PlotBase(BECWidget, QWidget):
|
||||
Args:
|
||||
curves (list): List of curves to apply autorange to.
|
||||
"""
|
||||
visible_curves = self._fetch_visible_curves()
|
||||
self.plot_item.autoRange(items=visible_curves if visible_curves else None)
|
||||
visible_items = self.visible_items
|
||||
|
||||
self.plot_item.autoRange(items=visible_items if visible_items else None)
|
||||
|
||||
@SafeProperty(int, doc="The font size of the legend font.")
|
||||
def legend_label_size(self) -> int:
|
||||
|
||||
@@ -42,10 +42,15 @@ class CurveConfig(ConnectionConfig):
|
||||
pen_style: Literal["solid", "dash", "dot", "dashdot"] | None = Field(
|
||||
"solid", description="The style of the pen of the curve."
|
||||
)
|
||||
source: Literal["device", "dap", "custom"] = Field(
|
||||
source: Literal["device", "dap", "custom", "history"] = Field(
|
||||
"custom", description="The source of the curve."
|
||||
)
|
||||
signal: DeviceSignal | None = Field(None, description="The signal of the curve.")
|
||||
scan_id: str | None = Field(None, description="Scan ID to be used when `source` is 'history'.")
|
||||
scan_number: int | None = Field(
|
||||
None, description="Scan index to be used when `source` is 'history'."
|
||||
)
|
||||
current_x_mode: str | None = Field(None, description="The current x mode of the history curve.")
|
||||
parent_label: str | None = Field(
|
||||
None, description="The label of the parent plot, only relevant for dap curves."
|
||||
)
|
||||
@@ -199,7 +204,7 @@ class Curve(BECConnector, pg.PlotDataItem):
|
||||
Raises:
|
||||
ValueError: If the source is not custom.
|
||||
"""
|
||||
if self.config.source == "custom":
|
||||
if self.config.source in ["custom", "history"]:
|
||||
self.setData(x, y)
|
||||
else:
|
||||
raise ValueError(f"Source {self.config.source} do not allow custom data setting.")
|
||||
|
||||
@@ -5,7 +5,34 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QValidator
|
||||
|
||||
|
||||
class ScanIndexValidator(QValidator):
|
||||
"""Validator to allow only 'live' or integer scan numbers from an allowed set."""
|
||||
|
||||
def __init__(self, allowed_scans: set[int] | None = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.allowed_scans = allowed_scans or set()
|
||||
|
||||
def validate(self, input_str: str, pos: int):
|
||||
# Accept empty or 'live'
|
||||
if input_str == "" or input_str == "live":
|
||||
return QValidator.State.Acceptable, input_str, pos
|
||||
# Allow partial editing of "live"
|
||||
if "live".startswith(input_str):
|
||||
return QValidator.State.Intermediate, input_str, pos
|
||||
# Accept integer only if present in the allowed set
|
||||
if input_str.isdigit():
|
||||
try:
|
||||
num = int(input_str)
|
||||
except ValueError:
|
||||
return QValidator.State.Invalid, input_str, pos
|
||||
if num in self.allowed_scans:
|
||||
return QValidator.State.Acceptable, input_str, pos
|
||||
return QValidator.State.Invalid, input_str, pos
|
||||
|
||||
|
||||
from qtpy.QtWidgets import (
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
@@ -91,8 +118,60 @@ class CurveRow(QTreeWidgetItem):
|
||||
# Create columns 1..2, depending on source
|
||||
self._init_source_ui()
|
||||
# Create columns 3..6 (color, style, width, symbol)
|
||||
self._init_scan_index_ui()
|
||||
self._init_style_controls()
|
||||
|
||||
def _init_scan_index_ui(self):
|
||||
"""Create the Scan # editable combobox in column 3."""
|
||||
if self.source not in ("device", "history"):
|
||||
return
|
||||
self.scan_index_combo = QComboBox()
|
||||
self.scan_index_combo.setEditable(True)
|
||||
# Populate 'live' and all available history scan indices
|
||||
self.scan_index_combo.addItem("live", None)
|
||||
|
||||
scan_number_list = []
|
||||
scan_id_list = []
|
||||
try:
|
||||
history = getattr(self.curve_tree.client, "history", None)
|
||||
if history is not None:
|
||||
scan_number_list = getattr(history, "_scan_numbers", []) or []
|
||||
scan_id_list = getattr(history, "_scan_ids", []) or []
|
||||
except Exception as e:
|
||||
logger.error(f"Cannot fetch scan numbers from BEC client: {e}")
|
||||
# If scan numbers cannot be fetched, only provide 'live' option
|
||||
scan_number_list = []
|
||||
scan_id_list = []
|
||||
|
||||
# Restrict input to 'live' or valid scan numbers
|
||||
allowed = set()
|
||||
try:
|
||||
allowed = set(int(n) for n in scan_number_list if isinstance(n, (int, str)))
|
||||
except Exception:
|
||||
allowed = set()
|
||||
validator = ScanIndexValidator(allowed, self.scan_index_combo)
|
||||
self.scan_index_combo.lineEdit().setValidator(validator)
|
||||
|
||||
# Add items: show scan numbers, store scan IDs as item data
|
||||
if scan_number_list and scan_id_list and len(scan_number_list) == len(scan_id_list):
|
||||
for num, sid in zip(scan_number_list, scan_id_list):
|
||||
self.scan_index_combo.addItem(str(num), sid)
|
||||
else:
|
||||
logger.error("Scan number and ID lists are mismatched or empty.")
|
||||
|
||||
# Select current based on existing config
|
||||
selected = False
|
||||
if getattr(self.config, "scan_id", None): # scan_id matching only
|
||||
for i in range(self.scan_index_combo.count()):
|
||||
if self.scan_index_combo.itemData(i) == self.config.scan_id:
|
||||
self.scan_index_combo.setCurrentIndex(i)
|
||||
selected = True
|
||||
break
|
||||
if not selected:
|
||||
self.scan_index_combo.setCurrentText("live")
|
||||
|
||||
self.tree.setItemWidget(self, 3, self.scan_index_combo)
|
||||
|
||||
def _init_actions(self):
|
||||
"""Create the actions widget in column 0, including a delete button and maybe 'Add DAP'."""
|
||||
self.actions_widget = QWidget()
|
||||
@@ -114,7 +193,7 @@ class CurveRow(QTreeWidgetItem):
|
||||
actions_layout.addWidget(self.delete_button)
|
||||
|
||||
# If device row, add "Add DAP" button
|
||||
if self.source == "device":
|
||||
if self.source in ("device", "history"):
|
||||
self.add_dap_button = QPushButton("DAP")
|
||||
self.add_dap_button.clicked.connect(lambda: self.add_dap_row())
|
||||
actions_layout.addWidget(self.add_dap_button)
|
||||
@@ -123,7 +202,7 @@ class CurveRow(QTreeWidgetItem):
|
||||
|
||||
def _init_source_ui(self):
|
||||
"""Create columns 1 and 2. For device rows, we have device/entry edits; for dap rows, label/model combo."""
|
||||
if self.source == "device":
|
||||
if self.source in ("device", "history"):
|
||||
# Device row: columns 1..2 are device line edits
|
||||
self.device_edit = DeviceComboBox(parent=self.tree)
|
||||
self.device_edit.insertItem(0, "")
|
||||
@@ -152,7 +231,6 @@ class CurveRow(QTreeWidgetItem):
|
||||
|
||||
self.tree.setItemWidget(self, 1, self.device_edit)
|
||||
self.tree.setItemWidget(self, 2, self.entry_edit)
|
||||
|
||||
else:
|
||||
# DAP row: column1= "Model" label, column2= DapComboBox
|
||||
self.label_widget = QLabel("Model")
|
||||
@@ -171,31 +249,31 @@ class CurveRow(QTreeWidgetItem):
|
||||
self.tree.setItemWidget(self, 2, self.dap_combo)
|
||||
|
||||
def _init_style_controls(self):
|
||||
"""Create columns 3..6: color button, style combo, width spin, symbol spin."""
|
||||
# Color in col 3
|
||||
"""Create columns 4..7: color button, style combo, width spin, symbol spin."""
|
||||
# Color in col 4
|
||||
self.color_button = ColorButtonNative(color=self.config.color)
|
||||
self.color_button.color_changed.connect(self._on_color_changed)
|
||||
self.tree.setItemWidget(self, 3, self.color_button)
|
||||
self.tree.setItemWidget(self, 4, self.color_button)
|
||||
|
||||
# Style in col 4
|
||||
# Style in col 5
|
||||
self.style_combo = QComboBox()
|
||||
self.style_combo.addItems(["solid", "dash", "dot", "dashdot"])
|
||||
idx = self.style_combo.findText(self.config.pen_style)
|
||||
if idx >= 0:
|
||||
self.style_combo.setCurrentIndex(idx)
|
||||
self.tree.setItemWidget(self, 4, self.style_combo)
|
||||
self.tree.setItemWidget(self, 5, self.style_combo)
|
||||
|
||||
# Pen width in col 5
|
||||
# Pen width in col 6
|
||||
self.width_spin = QSpinBox()
|
||||
self.width_spin.setRange(1, 20)
|
||||
self.width_spin.setValue(self.config.pen_width)
|
||||
self.tree.setItemWidget(self, 5, self.width_spin)
|
||||
self.tree.setItemWidget(self, 6, self.width_spin)
|
||||
|
||||
# Symbol size in col 6
|
||||
# Symbol size in col 7
|
||||
self.symbol_spin = QSpinBox()
|
||||
self.symbol_spin.setRange(1, 20)
|
||||
self.symbol_spin.setValue(self.config.symbol_size)
|
||||
self.tree.setItemWidget(self, 6, self.symbol_spin)
|
||||
self.tree.setItemWidget(self, 7, self.symbol_spin)
|
||||
|
||||
@SafeSlot(str, verify_sender=True)
|
||||
def _on_color_changed(self, new_color: str):
|
||||
@@ -209,8 +287,8 @@ class CurveRow(QTreeWidgetItem):
|
||||
self.config.symbol_color = new_color
|
||||
|
||||
def add_dap_row(self):
|
||||
"""Create a new DAP row as a child. Only valid if source='device'."""
|
||||
if self.source != "device":
|
||||
"""Create a new DAP row as a child. Only valid if source is 'device' or 'history'."""
|
||||
if self.source not in ("device", "history"):
|
||||
return
|
||||
curve_tree = self.tree.parent()
|
||||
parent_label = self.config.label
|
||||
@@ -288,7 +366,7 @@ class CurveRow(QTreeWidgetItem):
|
||||
Returns:
|
||||
dict: The serialized config based on the GUI state.
|
||||
"""
|
||||
if self.source == "device":
|
||||
if self.source in ("device", "history"):
|
||||
# Gather device name/entry
|
||||
device_name = ""
|
||||
device_entry = ""
|
||||
@@ -309,8 +387,23 @@ class CurveRow(QTreeWidgetItem):
|
||||
)
|
||||
|
||||
self.config.signal = DeviceSignal(name=device_name, entry=device_entry)
|
||||
self.config.source = "device"
|
||||
self.config.label = f"{device_name}-{device_entry}"
|
||||
scan_combo_text = self.scan_index_combo.currentText()
|
||||
if scan_combo_text == "live" or scan_combo_text == "":
|
||||
self.config.scan_number = None
|
||||
self.config.scan_id = None
|
||||
self.config.source = "device"
|
||||
self.config.label = f"{device_name}-{device_entry}"
|
||||
if scan_combo_text.isdigit():
|
||||
try:
|
||||
scan_num = int(scan_combo_text)
|
||||
except ValueError:
|
||||
scan_num = None
|
||||
self.config.scan_number = scan_num
|
||||
self.config.scan_id = self.scan_index_combo.currentData()
|
||||
self.config.source = "history"
|
||||
# Label history curves with scan number suffix
|
||||
if scan_num is not None:
|
||||
self.config.label = f"{device_name}-{device_entry}-scan-{scan_num}"
|
||||
else:
|
||||
# DAP logic
|
||||
parent_conf_dict = {}
|
||||
@@ -443,10 +536,12 @@ class CurveTree(BECWidget, QWidget):
|
||||
self.toolbar.show_bundles(["curve_tree"])
|
||||
|
||||
def _init_tree(self):
|
||||
"""Initialize the QTreeWidget with 7 columns and compact widths."""
|
||||
"""Initialize the QTreeWidget with 8 columns and compact widths."""
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setColumnCount(7)
|
||||
self.tree.setHeaderLabels(["Actions", "Name", "Entry", "Color", "Style", "Width", "Symbol"])
|
||||
self.tree.setColumnCount(8)
|
||||
self.tree.setHeaderLabels(
|
||||
["Actions", "Name", "Entry", "Scan #", "Color", "Style", "Width", "Symbol"]
|
||||
)
|
||||
|
||||
header = self.tree.header()
|
||||
for idx in range(self.tree.columnCount()):
|
||||
@@ -456,10 +551,10 @@ class CurveTree(BECWidget, QWidget):
|
||||
header.setSectionResizeMode(idx, QHeaderView.Fixed)
|
||||
header.setStretchLastSection(False)
|
||||
self.tree.setColumnWidth(0, 90)
|
||||
self.tree.setColumnWidth(3, 70)
|
||||
self.tree.setColumnWidth(4, 80)
|
||||
self.tree.setColumnWidth(5, 50)
|
||||
self.tree.setColumnWidth(4, 70)
|
||||
self.tree.setColumnWidth(5, 80)
|
||||
self.tree.setColumnWidth(6, 50)
|
||||
self.tree.setColumnWidth(7, 50)
|
||||
|
||||
self.layout.addWidget(self.tree)
|
||||
|
||||
@@ -583,9 +678,9 @@ class CurveTree(BECWidget, QWidget):
|
||||
self.tree.clear()
|
||||
self.all_items = []
|
||||
|
||||
device_curves = [c for c in self.waveform.curves if c.config.source == "device"]
|
||||
top_curves = [c for c in self.waveform.curves if c.config.source in ("device", "history")]
|
||||
dap_curves = [c for c in self.waveform.curves if c.config.source == "dap"]
|
||||
for dev in device_curves:
|
||||
for dev in top_curves:
|
||||
dr = CurveRow(self.tree, parent_item=None, config=dev.config, device_manager=self.dev)
|
||||
for dap in dap_curves:
|
||||
if dap.config.parent_label == dev.config.label:
|
||||
|
||||
@@ -8,6 +8,7 @@ import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.scan_data_container import ScanDataContainer
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from qtpy.QtCore import Qt, QTimer, Signal
|
||||
from qtpy.QtWidgets import (
|
||||
@@ -35,6 +36,9 @@ from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||
from bec_widgets.widgets.plots.waveform.curve import Curve, CurveConfig, DeviceSignal
|
||||
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_setting import CurveSetting
|
||||
from bec_widgets.widgets.plots.waveform.utils.roi_manager import WaveformROIManager
|
||||
from bec_widgets.widgets.services.scan_history_browser.scan_history_browser import (
|
||||
ScanHistoryBrowser,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -163,6 +167,7 @@ class Waveform(PlotBase):
|
||||
# Curve data
|
||||
self._sync_curves = []
|
||||
self._async_curves = []
|
||||
self._history_curves = []
|
||||
self._slice_index = None
|
||||
self._dap_curves = []
|
||||
self._mode = None
|
||||
@@ -179,12 +184,14 @@ class Waveform(PlotBase):
|
||||
"readout_priority": None,
|
||||
"label_suffix": "",
|
||||
}
|
||||
self._current_x_device: tuple[str, str] | None = None
|
||||
|
||||
# Specific GUI elements
|
||||
self._init_roi_manager()
|
||||
self.dap_summary = None
|
||||
self.dap_summary_dialog = None
|
||||
self._add_fit_parameters_popup()
|
||||
self.scan_history_dialog = None
|
||||
self._add_waveform_specific_popup()
|
||||
self._enable_roi_toolbar_action(False) # default state where are no dap curves
|
||||
self._init_curve_dialog()
|
||||
self.curve_settings_dialog = None
|
||||
@@ -252,7 +259,7 @@ class Waveform(PlotBase):
|
||||
super().add_side_menus()
|
||||
self._add_dap_summary_side_menu()
|
||||
|
||||
def _add_fit_parameters_popup(self):
|
||||
def _add_waveform_specific_popup(self):
|
||||
"""
|
||||
Add popups to the Waveform widget.
|
||||
"""
|
||||
@@ -262,11 +269,24 @@ class Waveform(PlotBase):
|
||||
icon_name="monitoring", tooltip="Open Fit Parameters", checkable=True, parent=self
|
||||
),
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"scan_history",
|
||||
MaterialIconAction(
|
||||
icon_name="manage_search",
|
||||
tooltip="Open Scan History browser",
|
||||
checkable=True,
|
||||
parent=self,
|
||||
),
|
||||
)
|
||||
self.toolbar.get_bundle("axis_popup").add_action("fit_params")
|
||||
self.toolbar.get_bundle("axis_popup").add_action("scan_history")
|
||||
|
||||
self.toolbar.components.get_action("fit_params").action.triggered.connect(
|
||||
self.show_dap_summary_popup
|
||||
)
|
||||
self.toolbar.components.get_action("scan_history").action.triggered.connect(
|
||||
self.show_scan_history_popup
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
def _reset_view(self):
|
||||
@@ -414,6 +434,47 @@ class Waveform(PlotBase):
|
||||
self.toolbar.components.get_action("roi_linear").action.setChecked(False)
|
||||
self._roi_manager.toggle_roi(False)
|
||||
|
||||
################################################################################
|
||||
# Scan History browser popup
|
||||
# TODO this is so far quick implementation just as popup, we should make scan history also standalone widget later
|
||||
def show_scan_history_popup(self):
|
||||
"""
|
||||
Show the scan history popup.
|
||||
"""
|
||||
scan_history_action = self.toolbar.components.get_action("scan_history").action
|
||||
if self.scan_history_dialog is None or not self.scan_history_dialog.isVisible():
|
||||
self.scan_history_widget = ScanHistoryBrowser(parent=self)
|
||||
self.scan_history_dialog = QDialog(modal=False)
|
||||
self.scan_history_dialog.setWindowTitle(f"{self.object_name} - Scan History Browser")
|
||||
self.scan_history_dialog.layout = QVBoxLayout(self.scan_history_dialog)
|
||||
self.scan_history_dialog.layout.addWidget(self.scan_history_widget)
|
||||
self.scan_history_widget.scan_history_device_viewer.request_history_plot.connect(
|
||||
lambda scan_id, device_name, signal_name: self.plot(
|
||||
y_name=device_name, y_entry=signal_name, scan_id=scan_id
|
||||
)
|
||||
)
|
||||
self.scan_history_dialog.finished.connect(self._scan_history_closed)
|
||||
self.scan_history_dialog.show()
|
||||
self.scan_history_dialog.resize(780, 320)
|
||||
scan_history_action.setChecked(True)
|
||||
else:
|
||||
# If already open, bring it to the front
|
||||
self.scan_history_dialog.raise_()
|
||||
self.scan_history_dialog.activateWindow()
|
||||
scan_history_action.setChecked(True) # keep it toggle
|
||||
|
||||
def _scan_history_closed(self):
|
||||
"""
|
||||
Slot for when the scan history dialog is closed.
|
||||
"""
|
||||
if self.scan_history_dialog is None:
|
||||
return
|
||||
self.scan_history_widget.close()
|
||||
self.scan_history_widget.deleteLater()
|
||||
self.scan_history_dialog.deleteLater()
|
||||
self.scan_history_dialog = None
|
||||
self.toolbar.components.get_action("scan_history").action.setChecked(False)
|
||||
|
||||
################################################################################
|
||||
# Dap Summary
|
||||
|
||||
@@ -503,7 +564,11 @@ class Waveform(PlotBase):
|
||||
self.x_axis_mode["name"] = value
|
||||
if value not in ["timestamp", "index", "auto"]:
|
||||
self.x_axis_mode["entry"] = self.entry_validator.validate_signal(value, None)
|
||||
self._current_x_device = (value, self.x_axis_mode["entry"])
|
||||
self._switch_x_axis_item(mode=value)
|
||||
self._current_x_device = None
|
||||
self._refresh_history_curves()
|
||||
self._update_curve_visibility()
|
||||
self.async_signal_update.emit()
|
||||
self.sync_signal_update.emit()
|
||||
self.plot_item.enableAutoRange(x=True)
|
||||
@@ -531,6 +596,8 @@ class Waveform(PlotBase):
|
||||
return
|
||||
self.x_axis_mode["entry"] = self.entry_validator.validate_signal(self.x_mode, value)
|
||||
self._switch_x_axis_item(mode="device")
|
||||
self._refresh_history_curves()
|
||||
self._update_curve_visibility()
|
||||
self.async_signal_update.emit()
|
||||
self.sync_signal_update.emit()
|
||||
self.plot_item.enableAutoRange(x=True)
|
||||
@@ -671,6 +738,8 @@ class Waveform(PlotBase):
|
||||
color: str | None = None,
|
||||
label: str | None = None,
|
||||
dap: str | None = None,
|
||||
scan_id: str | None = None,
|
||||
scan_number: int | None = None,
|
||||
**kwargs,
|
||||
) -> Curve:
|
||||
"""
|
||||
@@ -693,6 +762,10 @@ class Waveform(PlotBase):
|
||||
dap(str): The dap model to use for the curve, only available for sync devices.
|
||||
If not specified, none will be added.
|
||||
Use the same string as is the name of the LMFit model.
|
||||
scan_id(str): Optional scan ID. When provided, the curve is treated as a **history** curve and
|
||||
the y‑data (and optional x‑data) are fetched from that historical scan. Such curves are
|
||||
never cleared by live‑scan resets.
|
||||
scan_number(int): Optional scan index. When provided, the curve is treated as a **history** curve and
|
||||
|
||||
Returns:
|
||||
Curve: The curve object.
|
||||
@@ -762,6 +835,8 @@ class Waveform(PlotBase):
|
||||
label=label,
|
||||
color=color,
|
||||
source=source,
|
||||
scan_id=scan_id,
|
||||
scan_number=scan_number,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -769,6 +844,9 @@ class Waveform(PlotBase):
|
||||
if source == "device":
|
||||
config.signal = DeviceSignal(name=y_name, entry=y_entry)
|
||||
|
||||
if scan_id is not None or scan_number is not None:
|
||||
config.source = "history"
|
||||
|
||||
# CREATE THE CURVE
|
||||
curve = self._add_curve(config=config, x_data=x_data, y_data=y_data)
|
||||
|
||||
@@ -807,7 +885,7 @@ class Waveform(PlotBase):
|
||||
device_curve = self._find_curve_by_label(device_label)
|
||||
if not device_curve:
|
||||
raise ValueError(f"No existing curve found with label '{device_label}'.")
|
||||
if device_curve.config.source != "device":
|
||||
if device_curve.config.source not in ("device", "history"):
|
||||
raise ValueError(
|
||||
f"Curve '{device_label}' is not a device curve. Only device curves can have DAP."
|
||||
)
|
||||
@@ -816,7 +894,7 @@ class Waveform(PlotBase):
|
||||
dev_entry = device_curve.config.signal.entry
|
||||
|
||||
# 2) Build a label for the new DAP curve
|
||||
dap_label = f"{dev_name}-{dev_entry}-{dap_name}"
|
||||
dap_label = f"{device_label}-{dap_name}"
|
||||
|
||||
# 3) Possibly raise if the DAP curve already exists
|
||||
if self._check_curve_id(dap_label):
|
||||
@@ -869,7 +947,23 @@ class Waveform(PlotBase):
|
||||
ValueError: If a duplicate curve label/config is found, or if
|
||||
custom data is missing for `source='custom'`.
|
||||
"""
|
||||
scan_item: ScanDataContainer | None = None
|
||||
if config.source == "history":
|
||||
scan_item = self.get_history_scan_item(
|
||||
scan_id=config.scan_id, scan_index=config.scan_number
|
||||
)
|
||||
if scan_item is None:
|
||||
raise ValueError(
|
||||
f"Could not find scan item for history curve '{config.label}' with scan_id='{config.scan_id}' and scan_number='{config.scan_number}'."
|
||||
)
|
||||
|
||||
config.scan_id = scan_item.metadata["bec"]["scan_id"]
|
||||
config.scan_number = scan_item.metadata["bec"]["scan_number"]
|
||||
|
||||
label = config.label
|
||||
if config.source == "history":
|
||||
label = f"{config.signal.name}-{config.signal.entry}-scan-{config.scan_number}"
|
||||
config.label = label
|
||||
if not label:
|
||||
# Fallback label
|
||||
label = WidgetContainerUtils.generate_unique_name(
|
||||
@@ -891,7 +985,7 @@ class Waveform(PlotBase):
|
||||
raise ValueError("For 'custom' curves, x_data and y_data must be provided.")
|
||||
|
||||
# Actually create the Curve item
|
||||
curve = self._add_curve_object(name=label, config=config)
|
||||
curve = self._add_curve_object(name=label, config=config, scan_item=scan_item)
|
||||
|
||||
# If custom => set initial data
|
||||
if config.source == "custom" and x_data is not None and y_data is not None:
|
||||
@@ -908,6 +1002,8 @@ class Waveform(PlotBase):
|
||||
self.setup_dap_for_scan()
|
||||
self.roi_enable.emit(True) # Enable the ROI toolbar action
|
||||
self.request_dap() # Request DAP update directly without blocking proxy
|
||||
if config.source == "history":
|
||||
self._history_curves.append(curve)
|
||||
|
||||
QTimer.singleShot(
|
||||
150, self.auto_range
|
||||
@@ -915,22 +1011,182 @@ class Waveform(PlotBase):
|
||||
|
||||
return curve
|
||||
|
||||
def _add_curve_object(self, name: str, config: CurveConfig) -> Curve:
|
||||
def _add_curve_object(
|
||||
self, name: str, config: CurveConfig, scan_item: ScanDataContainer | None = None
|
||||
) -> Curve | None:
|
||||
"""
|
||||
Low-level creation of the PlotDataItem (Curve) from a `CurveConfig`.
|
||||
|
||||
Args:
|
||||
name (str): The name/label of the curve.
|
||||
config (CurveConfig): Configuration model describing the curve.
|
||||
scan_item (ScanDataContainer | None): Optional scan item for history curves.
|
||||
|
||||
Returns:
|
||||
Curve: The newly created curve object, added to the plot.
|
||||
"""
|
||||
curve = Curve(config=config, name=name, parent_item=self)
|
||||
self.plot_item.addItem(curve)
|
||||
if scan_item is not None:
|
||||
self._fetch_history_data_for_curve(curve, scan_item)
|
||||
self._categorise_device_curves()
|
||||
curve.visibleChanged.connect(self._refresh_crosshair_markers)
|
||||
curve.visibleChanged.connect(self.auto_range)
|
||||
return curve
|
||||
|
||||
def _fetch_history_data_for_curve(
|
||||
self, curve: Curve, scan_item: ScanDataContainer
|
||||
) -> Curve | None:
|
||||
# Check if the data are already set
|
||||
device = curve.config.signal.name
|
||||
entry = curve.config.signal.entry
|
||||
|
||||
all_devices_used = getattr(
|
||||
getattr(scan_item, "_msg", None), "stored_data_info", None
|
||||
) or getattr(scan_item, "stored_data_info", None)
|
||||
if all_devices_used is None:
|
||||
curve.remove()
|
||||
raise ValueError(
|
||||
f"No stored data info found in scan item ID:{curve.config.scan_id} for curve '{curve.name()}'. "
|
||||
f"Upgrade BEC to the latest version."
|
||||
)
|
||||
|
||||
# 1. get y data
|
||||
x_data, y_data = None, None
|
||||
if device not in all_devices_used:
|
||||
raise ValueError(f"Device '{device}' not found in scan item ID:{curve.config.scan_id}.")
|
||||
if entry not in all_devices_used[device]:
|
||||
raise ValueError(
|
||||
f"Entry '{entry}' not found in device '{device}' in scan item ID:{curve.config.scan_id}."
|
||||
)
|
||||
y_shape = all_devices_used.get(device).get(entry).shape[0]
|
||||
|
||||
# Determine X-axis data
|
||||
if self.x_axis_mode["name"] == "index":
|
||||
x_data = np.arange(y_shape)
|
||||
curve.config.current_x_mode = "index"
|
||||
self._update_x_label_suffix(" (index)")
|
||||
elif self.x_axis_mode["name"] == "timestamp":
|
||||
y_device = scan_item.devices.get(device)
|
||||
x_data = y_device.get(entry).read().get("timestamp")
|
||||
curve.config.current_x_mode = "timestamp"
|
||||
self._update_x_label_suffix(" (timestamp)")
|
||||
elif self.x_axis_mode["name"] not in ("index", "timestamp", "auto"): # Custom device mode
|
||||
if self.x_axis_mode["name"] not in all_devices_used:
|
||||
logger.warning(
|
||||
f"Custom device '{self.x_axis_mode['name']}' not found in scan item of history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
|
||||
)
|
||||
curve.setVisible(False)
|
||||
return
|
||||
x_entry_custom = self.x_axis_mode.get("entry")
|
||||
if x_entry_custom is None:
|
||||
x_entry_custom = self.entry_validator.validate_signal(
|
||||
self.x_axis_mode["name"], None
|
||||
)
|
||||
if x_entry_custom not in all_devices_used[self.x_axis_mode["name"]]:
|
||||
logger.warning(
|
||||
f"Custom entry '{x_entry_custom}' for device '{self.x_axis_mode['name']}' not found in scan item of history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
|
||||
)
|
||||
curve.setVisible(False)
|
||||
return
|
||||
x_shape = (
|
||||
scan_item._msg.stored_data_info.get(self.x_axis_mode["name"])
|
||||
.get(x_entry_custom)
|
||||
.shape[0]
|
||||
)
|
||||
if x_shape != y_shape:
|
||||
logger.warning(
|
||||
f"Shape mismatch for x data '{x_shape}' and y data '{y_shape}' in history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
|
||||
)
|
||||
curve.setVisible(False)
|
||||
return
|
||||
x_device = scan_item.devices.get(self.x_axis_mode["name"])
|
||||
x_data = x_device.get(x_entry_custom).read().get("value")
|
||||
curve.config.current_x_mode = self.x_axis_mode["name"]
|
||||
self._update_x_label_suffix(f" (custom: {self.x_axis_mode['name']}-{x_entry_custom})")
|
||||
elif self.x_axis_mode["name"] == "auto":
|
||||
if (
|
||||
self._current_x_device is None
|
||||
): # Scenario where no x device is set yet, because there was no live scan done in this widget yet
|
||||
# If no current x device, use the first motor from scan item
|
||||
scan_motors = self._ensure_str_list(
|
||||
scan_item.metadata.get("bec").get("scan_report_devices")
|
||||
)
|
||||
if not scan_motors: # scan was done without reported motor from whatever reason
|
||||
x_data = np.arange(y_shape) # Fallback to index
|
||||
y_data = scan_item.devices.get(device).get(entry).read().get("value")
|
||||
curve.set_data(x=x_data, y=y_data)
|
||||
self._update_x_label_suffix(" (auto: index)")
|
||||
return curve
|
||||
x_entry = self.entry_validator.validate_signal(scan_motors[0], None)
|
||||
if x_entry not in all_devices_used.get(scan_motors[0], {}):
|
||||
logger.warning(
|
||||
f"Auto x entry '{x_entry}' for device '{scan_motors[0]}' not found in scan item of history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
|
||||
)
|
||||
curve.setVisible(False)
|
||||
return
|
||||
if y_shape != all_devices_used.get(scan_motors[0]).get(x_entry, {}).shape[0]:
|
||||
logger.warning(
|
||||
f"Shape mismatch for x data '{all_devices_used.get(scan_motors[0]).get(x_entry, {}).get('shape', [0])[0]}' and y data '{y_shape}' in history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
|
||||
)
|
||||
curve.setVisible(False)
|
||||
return
|
||||
x_data = scan_item.devices.get(scan_motors[0]).get(x_entry).read().get("value")
|
||||
self._current_x_device = (scan_motors[0], x_entry)
|
||||
self._update_x_label_suffix(f" (auto: {scan_motors[0]}-{x_entry})")
|
||||
curve.config.current_x_mode = "auto"
|
||||
self._update_x_label_suffix(f" (auto: {scan_motors[0]}-{x_entry})")
|
||||
else: # Scan in auto mode was done and live scan already set the current x device
|
||||
if self._current_x_device[0] not in all_devices_used:
|
||||
logger.warning(
|
||||
f"Auto x data for device '{self._current_x_device[0]}' "
|
||||
f"and entry '{self._current_x_device[1]}'"
|
||||
f" not found in scan item of the history curve {curve.name()}."
|
||||
)
|
||||
curve.setVisible(False)
|
||||
return
|
||||
x_device = scan_item.devices.get(self._current_x_device[0])
|
||||
x_data = x_device.get(self._current_x_device[1]).read().get("value")
|
||||
curve.config.current_x_mode = "auto"
|
||||
self._update_x_label_suffix(
|
||||
f" (auto: {self._current_x_device[0]}-{self._current_x_device[1]})"
|
||||
)
|
||||
if x_data is None:
|
||||
logger.warning(
|
||||
f"X data for curve '{curve.name()}' could not be determined. "
|
||||
f"Check if the x_mode '{self.x_axis_mode['name']}' is valid for the scan item."
|
||||
)
|
||||
curve.setVisible(False)
|
||||
return
|
||||
if y_data is None:
|
||||
y_data = scan_item.devices.get(device).get(entry).read().get("value")
|
||||
if y_data is None:
|
||||
logger.warning(
|
||||
f"Y data for curve '{curve.name()}' could not be determined. "
|
||||
f"Check if the device '{device}' and entry '{entry}' are valid for the scan item."
|
||||
)
|
||||
curve.setVisible(False)
|
||||
return
|
||||
curve.set_data(x=x_data, y=y_data)
|
||||
return curve
|
||||
|
||||
def _refresh_history_curves(self):
|
||||
for curve in self._history_curves:
|
||||
scan_item = self.get_history_scan_item(
|
||||
scan_id=curve.config.scan_id, scan_index=curve.config.scan_number
|
||||
)
|
||||
if scan_item is not None:
|
||||
self._fetch_history_data_for_curve(curve, scan_item)
|
||||
else:
|
||||
logger.warning(f"Scan item for curve {curve.name()} not found.")
|
||||
|
||||
def _refresh_crosshair_markers(self):
|
||||
"""
|
||||
Refresh the crosshair markers when a curve visibility changes.
|
||||
"""
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.clear_markers()
|
||||
|
||||
def _generate_color_from_palette(self) -> str:
|
||||
"""
|
||||
Generate a color for the next new curve, based on the current number of curves.
|
||||
@@ -958,7 +1214,42 @@ class Waveform(PlotBase):
|
||||
Clear all data from the plot widget, but keep the curve references.
|
||||
"""
|
||||
for c in self.curves:
|
||||
c.clear_data()
|
||||
if c.config.source != "history":
|
||||
c.clear_data()
|
||||
|
||||
# X-axis compatibility helpers
|
||||
def _is_curve_compatible(self, curve: Curve) -> bool:
|
||||
"""
|
||||
Return True when *curve* can be shown with the current x-axis mode.
|
||||
|
||||
- ‘index’, ‘timestamp’ are always compatible.
|
||||
- For history curves we check whether the requested motor
|
||||
(self.x_axis_mode["name"]) exists in the cached
|
||||
history_data_buffer["x"] dictionary.
|
||||
- DAP is done by checking if the parent curve is visible.
|
||||
- Device curves are fetched by update sync/async curves, which solves the compatibility there.
|
||||
"""
|
||||
mode = self.x_axis_mode.get("name", "index")
|
||||
if mode in ("index", "timestamp"): # always compatible - wild west mode
|
||||
return True
|
||||
if curve.config.source == "history":
|
||||
scan_item = self.get_history_scan_item(
|
||||
scan_id=curve.config.scan_id, scan_index=curve.config.scan_number
|
||||
)
|
||||
curve = self._fetch_history_data_for_curve(curve, scan_item)
|
||||
if curve is None:
|
||||
return False
|
||||
if curve.config.source == "dap":
|
||||
parent_curve = self._find_curve_by_label(curve.config.parent_label)
|
||||
if parent_curve.isVisible():
|
||||
return True
|
||||
return False # DAP curve is not compatible if parent curve is not visible
|
||||
return True
|
||||
|
||||
def _update_curve_visibility(self) -> None:
|
||||
"""Show or hide curves according to `_is_curve_compatible`."""
|
||||
for c in self.curves:
|
||||
c.setVisible(self._is_curve_compatible(c))
|
||||
|
||||
def clear_all(self):
|
||||
"""
|
||||
@@ -1115,12 +1406,13 @@ class Waveform(PlotBase):
|
||||
self.reset()
|
||||
self.new_scan.emit()
|
||||
self.new_scan_id.emit(current_scan_id)
|
||||
self.auto_range(True)
|
||||
self.auto_range_x = True
|
||||
self.auto_range_y = True
|
||||
self.old_scan_id = self.scan_id
|
||||
self.scan_id = current_scan_id
|
||||
self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id) # live scan
|
||||
self._slice_index = None # Reset the slice index
|
||||
|
||||
self._update_curve_visibility()
|
||||
self._mode = self._categorise_device_curves()
|
||||
|
||||
# First trigger to sync and async data
|
||||
@@ -1198,7 +1490,7 @@ class Waveform(PlotBase):
|
||||
device_data = entry_obj.read()["value"] if entry_obj else None
|
||||
x_data = self._get_x_data(device_name, device_entry)
|
||||
if x_data is not None:
|
||||
if len(x_data) == 1:
|
||||
if np.isscalar(x_data):
|
||||
self.clear_data()
|
||||
return
|
||||
if device_data is not None and x_data is not None:
|
||||
@@ -1606,6 +1898,7 @@ class Waveform(PlotBase):
|
||||
entry_obj = data.get(x_name, {}).get(x_entry)
|
||||
x_data = entry_obj.read()["value"] if entry_obj else [0]
|
||||
new_suffix = f" (custom: {x_name}-{x_entry})"
|
||||
self._current_x_device = (x_name, x_entry)
|
||||
|
||||
# 2 User wants timestamp
|
||||
if self.x_axis_mode["name"] == "timestamp":
|
||||
@@ -1620,11 +1913,13 @@ class Waveform(PlotBase):
|
||||
timestamps = entry_obj.read()["timestamp"] if entry_obj else [0]
|
||||
x_data = timestamps
|
||||
new_suffix = " (timestamp)"
|
||||
self._current_x_device = None
|
||||
|
||||
# 3 User wants index
|
||||
if self.x_axis_mode["name"] == "index":
|
||||
x_data = None
|
||||
new_suffix = " (index)"
|
||||
self._current_x_device = None
|
||||
|
||||
# 4 Best effort automatic mode
|
||||
if self.x_axis_mode["name"] is None or self.x_axis_mode["name"] == "auto":
|
||||
@@ -1632,6 +1927,7 @@ class Waveform(PlotBase):
|
||||
if len(self._async_curves) > 0:
|
||||
x_data = None
|
||||
new_suffix = " (auto: index)"
|
||||
self._current_x_device = None
|
||||
# 4.2 If there are sync curves, use the first device from the scan report
|
||||
else:
|
||||
try:
|
||||
@@ -1654,6 +1950,7 @@ class Waveform(PlotBase):
|
||||
entry_obj = data.get(x_name, {}).get(x_entry)
|
||||
x_data = entry_obj.read()["value"] if entry_obj else None
|
||||
new_suffix = f" (auto: {x_name}-{x_entry})"
|
||||
self._current_x_device = (x_name, x_entry)
|
||||
self._update_x_label_suffix(new_suffix)
|
||||
return x_data
|
||||
|
||||
@@ -1756,49 +2053,83 @@ class Waveform(PlotBase):
|
||||
logger.info(f"Scan {self.scan_id} => mode={self._mode}")
|
||||
return mode
|
||||
|
||||
@SafeSlot(int)
|
||||
@SafeSlot(str)
|
||||
@SafeSlot()
|
||||
def update_with_scan_history(self, scan_index: int = None, scan_id: str = None):
|
||||
def get_history_scan_item(
|
||||
self, scan_index: int = None, scan_id: str = None
|
||||
) -> ScanDataContainer | None:
|
||||
"""
|
||||
Update the scan curves with the data from the scan storage.
|
||||
Provide only one of scan_id or scan_index.
|
||||
Get scan item from history based on scan_id or scan_index.
|
||||
If both are provided, scan_id takes precedence and the resolved scan_number
|
||||
will be read from the fetched item.
|
||||
|
||||
Args:
|
||||
scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
|
||||
scan_index(int, optional): Index of the scan to be updated. Defaults to None.
|
||||
scan_id (str, optional): ScanID of the scan to fetch. Defaults to None.
|
||||
scan_index (int, optional): Index (scan number) of the scan to fetch. Defaults to None.
|
||||
|
||||
Returns:
|
||||
ScanDataContainer | None: The fetched scan item or None if no item was found.
|
||||
"""
|
||||
if scan_index is not None and scan_id is not None:
|
||||
raise ValueError("Only one of scan_id or scan_index can be provided.")
|
||||
scan_index = None # Prefer scan_id when both are given
|
||||
|
||||
if scan_index is None and scan_id is None:
|
||||
logger.warning(f"Neither scan_id or scan_number was provided, fetching the latest scan")
|
||||
logger.warning("Neither scan_id or scan_number was provided, fetching the latest scan")
|
||||
scan_index = -1
|
||||
|
||||
if scan_index is None:
|
||||
self.scan_id = scan_id
|
||||
self.scan_item = self.client.history.get_by_scan_id(scan_id)
|
||||
self._emit_signal_update()
|
||||
return
|
||||
return self.client.history.get_by_scan_id(scan_id)
|
||||
|
||||
if scan_index == -1:
|
||||
scan_item = self.client.queue.scan_storage.current_scan
|
||||
if scan_item is not None:
|
||||
if scan_item.status_message is None:
|
||||
logger.warning(f"Scan item with {scan_item.scan_id} has no status message.")
|
||||
return
|
||||
self.scan_item = scan_item
|
||||
self.scan_id = scan_item.scan_id
|
||||
self._emit_signal_update()
|
||||
return
|
||||
return None
|
||||
return scan_item
|
||||
|
||||
if len(self.client.history) == 0:
|
||||
logger.info("No scans executed so far. Skipping scan history update.")
|
||||
logger.info("No scans executed so far. Cannot fetch scan history.")
|
||||
return None
|
||||
|
||||
# check if scan_index is negative, then fetch it just from the list from the end
|
||||
if int(scan_index) < 0:
|
||||
return self.client.history[scan_index]
|
||||
scan_item = self.client.history.get_by_scan_number(scan_index)
|
||||
if scan_item is None:
|
||||
logger.warning(f"Scan with scan_number {scan_index} not found in history.")
|
||||
return None
|
||||
if isinstance(scan_item, list):
|
||||
if len(scan_item) > 1:
|
||||
logger.warning(
|
||||
f"Multiple scans found with scan_number {scan_index}. Returning the latest one."
|
||||
)
|
||||
scan_item = scan_item[-1]
|
||||
return scan_item
|
||||
|
||||
@SafeSlot(int)
|
||||
@SafeSlot(str)
|
||||
@SafeSlot()
|
||||
def update_with_scan_history(self, scan_index: int = None, scan_id: str = None):
|
||||
"""
|
||||
Update the scan curves with the data from the scan storage.
|
||||
If both arguments are provided, scan_id takes precedence and scan_index is ignored.
|
||||
|
||||
Args:
|
||||
scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
|
||||
scan_index(int, optional): Index (scan number) of the scan to be updated. Defaults to None.
|
||||
"""
|
||||
self.scan_item = self.get_history_scan_item(scan_index=scan_index, scan_id=scan_id)
|
||||
|
||||
if self.scan_item is None:
|
||||
return
|
||||
|
||||
self.scan_item = self.client.history[scan_index]
|
||||
metadata = self.scan_item.metadata
|
||||
self.scan_id = metadata["bec"]["scan_id"]
|
||||
if scan_id is not None:
|
||||
self.scan_id = scan_id
|
||||
else:
|
||||
# If scan_number was used, set the scan_id from the fetched item
|
||||
if hasattr(self.scan_item, "metadata"):
|
||||
self.scan_id = self.scan_item.metadata["bec"]["scan_id"]
|
||||
else:
|
||||
self.scan_id = self.scan_item.scan_id
|
||||
|
||||
self._emit_signal_update()
|
||||
|
||||
@@ -2029,6 +2360,9 @@ class Waveform(PlotBase):
|
||||
if self.dap_summary_dialog is not None:
|
||||
self.dap_summary_dialog.reject()
|
||||
self.dap_summary_dialog = None
|
||||
if self.scan_history_dialog is not None:
|
||||
self.scan_history_dialog.reject()
|
||||
self.scan_history_dialog = None
|
||||
super().cleanup()
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
|
||||
|
||||
class ProgressbarConnections(BaseModel):
|
||||
slot: Literal["on_scan_progress", "on_device_readback"] = None
|
||||
endpoint: EndpointInfo | str = None
|
||||
slot: Literal["on_scan_progress", "on_device_readback", None] = None
|
||||
endpoint: EndpointInfo | str | None = None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
@field_validator("endpoint")
|
||||
@@ -222,9 +222,10 @@ class Ring(BECConnector, QObject):
|
||||
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
|
||||
)
|
||||
if self.config.connections.slot is not None:
|
||||
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":
|
||||
|
||||
@@ -22,13 +22,9 @@ class RingProgressBarConfig(ConnectionConfig):
|
||||
color_map: Optional[str] = Field(
|
||||
"plasma", 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."
|
||||
)
|
||||
max_number_of_bars: int | None = Field(
|
||||
10, description="Maximum number of progress bars to display."
|
||||
)
|
||||
num_bars: int | None = Field(1, description="Number of progress bars to display.")
|
||||
min_number_of_bars: int = Field(1, description="Minimum number of progress bars to display.")
|
||||
max_number_of_bars: int = Field(10, description="Maximum number of progress bars to display.")
|
||||
num_bars: int = Field(1, description="Number of progress bars to display.")
|
||||
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."
|
||||
@@ -242,7 +238,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
for i, ring in enumerate(self._rings):
|
||||
ring.config.index = i
|
||||
|
||||
def set_precision(self, precision: int, bar_index: int = None):
|
||||
def set_precision(self, precision: int, bar_index: int | None = None):
|
||||
"""
|
||||
Set the precision for the progress bars. If bar_index is not provide, the precision will be set for all progress bars.
|
||||
|
||||
@@ -271,9 +267,9 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
min_values(int|float | list[float]): Minimum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of minimum values for each progress bar.
|
||||
max_values(int|float | list[float]): Maximum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of maximum values for each progress bar.
|
||||
"""
|
||||
if isinstance(min_values, int) or isinstance(min_values, float):
|
||||
if isinstance(min_values, (int, float)):
|
||||
min_values = [min_values]
|
||||
if isinstance(max_values, int) or isinstance(max_values, float):
|
||||
if isinstance(max_values, (int, float)):
|
||||
max_values = [max_values]
|
||||
min_values = self._adjust_list_to_bars(min_values)
|
||||
max_values = self._adjust_list_to_bars(max_values)
|
||||
@@ -441,14 +437,10 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
Returns:
|
||||
Ring: Ring object.
|
||||
"""
|
||||
found_ring = None
|
||||
for ring in self._rings:
|
||||
if ring.config.index == index:
|
||||
found_ring = ring
|
||||
break
|
||||
if found_ring is None:
|
||||
raise ValueError(f"Ring with index {index} not found.")
|
||||
return found_ring
|
||||
return ring
|
||||
raise ValueError(f"Ring with index {index} not found.")
|
||||
|
||||
def enable_auto_updates(self, enable: bool = True):
|
||||
"""
|
||||
@@ -485,29 +477,30 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
primary_queue = msg.get("queue").get("primary")
|
||||
info = primary_queue.get("info", None)
|
||||
|
||||
if info:
|
||||
active_request_block = info[0].get("active_request_block", None)
|
||||
if active_request_block:
|
||||
report_instructions = active_request_block.get("report_instructions", None)
|
||||
if report_instructions:
|
||||
instruction_type = list(report_instructions[0].keys())[0]
|
||||
if instruction_type == "scan_progress":
|
||||
self._hook_scan_progress(ring_index=0)
|
||||
elif instruction_type == "readback":
|
||||
devices = report_instructions[0].get("readback").get("devices")
|
||||
start = report_instructions[0].get("readback").get("start")
|
||||
end = report_instructions[0].get("readback").get("end")
|
||||
if self.config.num_bars != len(devices):
|
||||
self.set_number_of_bars(len(devices))
|
||||
for index, device in enumerate(devices):
|
||||
self._hook_readback(index, device, start[index], end[index])
|
||||
else:
|
||||
logger.error(f"{instruction_type} not supported yet.")
|
||||
if not info:
|
||||
return
|
||||
active_request_block = info[0].get("active_request_block", None)
|
||||
if not active_request_block:
|
||||
return
|
||||
report_instructions = active_request_block.get("report_instructions", None)
|
||||
if not report_instructions:
|
||||
return
|
||||
|
||||
# elif instruction_type == "device_progress":
|
||||
# print("hook device_progress")
|
||||
instruction_type = list(report_instructions[0].keys())[0]
|
||||
if instruction_type == "scan_progress":
|
||||
self._hook_scan_progress(ring_index=0)
|
||||
elif instruction_type == "readback":
|
||||
devices = report_instructions[0].get("readback").get("devices")
|
||||
start = report_instructions[0].get("readback").get("start")
|
||||
end = report_instructions[0].get("readback").get("end")
|
||||
if self.config.num_bars != len(devices):
|
||||
self.set_number_of_bars(len(devices))
|
||||
for index, device in enumerate(devices):
|
||||
self._hook_readback(index, device, start[index], end[index])
|
||||
else:
|
||||
logger.error(f"{instruction_type} not supported yet.")
|
||||
|
||||
def _hook_scan_progress(self, ring_index: int = None):
|
||||
def _hook_scan_progress(self, ring_index: int | None = None):
|
||||
"""
|
||||
Hook the scan progress to the progress bars.
|
||||
|
||||
@@ -521,8 +514,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
|
||||
if ring.config.connections.slot == "on_scan_progress":
|
||||
return
|
||||
else:
|
||||
ring.set_connections("on_scan_progress", MessageEndpoints.scan_progress())
|
||||
ring.set_connections("on_scan_progress", MessageEndpoints.scan_progress())
|
||||
|
||||
def _hook_readback(self, bar_index: int, device: str, min: float | int, max: float | int):
|
||||
"""
|
||||
@@ -576,6 +568,8 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
return bar_index
|
||||
|
||||
def paintEvent(self, event):
|
||||
if not self._rings:
|
||||
return
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
size = min(self.width(), self.height())
|
||||
@@ -628,9 +622,8 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
return QSize(10, 10)
|
||||
ring_widths = [self.config.rings[i].line_width for i in range(self.config.num_bars)]
|
||||
total_width = sum(ring_widths) + self.config.gap * (self.config.num_bars - 1)
|
||||
diameter = total_width * 2
|
||||
if diameter < 50:
|
||||
diameter = 50
|
||||
diameter = max(total_width * 2, 50)
|
||||
|
||||
return QSize(diameter, diameter)
|
||||
|
||||
def sizeHint(self):
|
||||
|
||||
@@ -38,8 +38,8 @@ class SignalDisplay(BECWidget, QWidget):
|
||||
@SafeSlot()
|
||||
def _refresh(self):
|
||||
if (dev := self.dev.get(self.device)) is not None:
|
||||
dev.read()
|
||||
dev.read_configuration()
|
||||
dev.read(cached=True)
|
||||
dev.read_configuration(cached=True)
|
||||
|
||||
def _add_refresh_button(self):
|
||||
button_holder = QWidget()
|
||||
|
||||
@@ -273,7 +273,9 @@ class SignalLabel(BECWidget, QWidget):
|
||||
if not isinstance(self._device_obj, Device | Signal):
|
||||
self._value, self._units = "__", ""
|
||||
return
|
||||
reading = (self._device_obj.read() or {}) | (self._device_obj.read_configuration() or {})
|
||||
reading = (self._device_obj.read(cached=True) or {}) | (
|
||||
self._device_obj.read_configuration(cached=True) or {}
|
||||
)
|
||||
value = reading.get(self._signal_key, {}).get("value")
|
||||
if value is None:
|
||||
self._value, self._units = "__", ""
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
(api_reference)=
|
||||
# API Reference
|
||||
|
||||
```{eval-rst}
|
||||
.. autosummary::
|
||||
:toctree: _autosummary
|
||||
:template: custom-module-template.rst
|
||||
:recursive:
|
||||
This page contains the auto-generated API documentation for all modules, classes, and functions in the BEC Widgets package.
|
||||
|
||||
bec_widgets
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
:caption: API Documentation
|
||||
|
||||
../autoapi/bec_widgets/index
|
||||
```
|
||||
42
docs/conf.py
42
docs/conf.py
@@ -32,16 +32,15 @@ def get_version():
|
||||
release = get_version()
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.autosummary",
|
||||
# "sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx_toolbox.collapse",
|
||||
"sphinx_copybutton",
|
||||
"myst_parser",
|
||||
"sphinx_design",
|
||||
"sphinx_inline_tabs",
|
||||
"autoapi.extension",
|
||||
"sphinx.ext.viewcode",
|
||||
]
|
||||
|
||||
myst_enable_extensions = [
|
||||
@@ -60,7 +59,15 @@ myst_enable_extensions = [
|
||||
"tasklist",
|
||||
]
|
||||
|
||||
autosummary_generate = True # Turn on sphinx.ext.autosummary
|
||||
# AutoAPI configuration
|
||||
autoapi_dirs = ["../bec_widgets"]
|
||||
autoapi_type = "python"
|
||||
autoapi_generate_api_docs = True
|
||||
autoapi_add_toctree_entry = False # We'll control the toctree manually
|
||||
autoapi_keep_files = False
|
||||
autoapi_python_class_content = "both" # Include both class docstring and __init__
|
||||
autoapi_member_order = "groupwise"
|
||||
|
||||
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
|
||||
@@ -80,3 +87,30 @@ html_theme = "pydata_sphinx_theme"
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = ["custom.css"]
|
||||
html_logo = "../bec_widgets/assets/app_icons/bec_widgets_icon.png"
|
||||
|
||||
|
||||
def skip_submodules(app, what, name, obj, skip, options):
|
||||
if what == "module":
|
||||
if not name.startswith("bec_widgets"):
|
||||
skip = True
|
||||
# print(f"Checking module: {name}")
|
||||
if "bec_widgets.widgets" in name:
|
||||
widget = name.split(".")[-2]
|
||||
submodule = name.split(".")[-1]
|
||||
if submodule in [f"register_{widget}", f"{widget}_plugin"]:
|
||||
# print(f"Skipping submodule: {name}")
|
||||
skip = True
|
||||
elif what in ["data", "attribute"]:
|
||||
obj_name = name.split(".")[-1]
|
||||
if obj_name.startswith("_") or obj_name in ["__all__", "logger", "bec_logger", "app"]:
|
||||
skip = True
|
||||
|
||||
elif what == "class":
|
||||
class_name = name.split(".")[-1]
|
||||
if class_name.startswith("Demo"):
|
||||
skip = True
|
||||
return skip
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.connect("autoapi-skip-member", skip_submodules)
|
||||
|
||||
@@ -10,5 +10,5 @@ We offer up to three different options for composing larger GUIs from these modu
|
||||
## Client-Server Architecture
|
||||
|
||||
BEC Widgets is built on top of the [BEC](https://bec.readthedocs.io/en/latest/) package, which provides the backend for beamline experiment control. BEC Widgets is a client of BEC, meaning it can interact with the backend through a client-server architecture. To make full usage of the available features of BEC, we recommend to check the documentation about [data access](https://bec.readthedocs.io/en/latest/developer/data_access/data_access.html) in which the messaging and event system of BEC is described.
|
||||
In the context of BEC Widgets, the [`BECDispatcher`](/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher) connects to this messaging and event system, allowing you to link your Qt [`Slots`](https://www.pythonguis.com/tutorials/pyside6-signals-slots-events/) to messages and event received from BEC.
|
||||
In the context of BEC Widgets, the {py:class}`~bec_widgets.utils.bec_dispatcher.BECDispatcher` connects to this messaging and event system, allowing you to link your Qt [`Slots`](https://www.pythonguis.com/tutorials/pyside6-signals-slots-events/) to messages and event received from BEC.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Therefore, we recommend that you install BEC first following the [developer inst
|
||||
If you already have a BEC environment set up, you can install BEC Widgets in editable mode into your BEC Python environment.
|
||||
|
||||
**Prerequisites**
|
||||
1. **Python Version:** BEC Widgets requires Python version 3.10 or higher. Verify your Python version to ensure compatibility.
|
||||
1. **Python Version:** BEC Widgets requires Python version 3.11 or higher. Verify your Python version to ensure compatibility.
|
||||
2. **BEC Installation:** BEC Widgets works in conjunction with BEC. While BEC is a dependency and will be installed automatically, you can find more information about BEC and its installation process in the [BEC documentation](https://beamline-experiment-control.readthedocs.io/en/latest/).
|
||||
3. **Qt Distributions:** BEC Widgets supports [PySide6](https://doc.qt.io/qtforpython-6/quickstart.html) and [PyQt6](https://www.riverbankcomputing.com/static/Docs/PyQt6/introduction.html). We use [qtpy](https://pypi.org/project/QtPy/) to abstract the underlying QT distribution.
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ sphinx-copybutton
|
||||
sphinx-inline-tabs
|
||||
myst-parser
|
||||
sphinx-design
|
||||
sphinx-autoapi
|
||||
tomli
|
||||
@@ -1,11 +1,10 @@
|
||||
(user.api_reference)=
|
||||
# User API Reference
|
||||
|
||||
```{eval-rst}
|
||||
.. autosummary::
|
||||
:toctree: _autosummary
|
||||
:template: custom-module-template.rst
|
||||
This section contains the API documentation for the main user-facing modules and classes.
|
||||
|
||||
bec_widgets.cli.client
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
../../autoapi/bec_widgets/cli/index
|
||||
```
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Before installing BEC Widgets, please ensure the following requirements are met:
|
||||
|
||||
1. **Python Version:** BEC Widgets requires Python version 3.10 or higher. Verify your Python version to ensure compatibility.
|
||||
1. **Python Version:** BEC Widgets requires Python version 3.11 or higher. Verify your Python version to ensure compatibility.
|
||||
2. **BEC Installation:** BEC Widgets works in conjunction with BEC. While BEC is a dependency and will be installed automatically, you can find more information about BEC and its installation process in the [BEC documentation](https://beamline-experiment-control.readthedocs.io/en/latest/).
|
||||
|
||||
**Standard Installation**
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
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 `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.
|
||||
|
||||
## BECGuiClient
|
||||
The `gui` object is the main entry point for interacting with the BEC Widgets framework. It is an instance of the [`BECGuiClient`](/api_reference/_autosummary/bec_widgets.cli.client.BECGuiClient) class, which provides methods to create and manage GUI components. Upon BEC startup, a default [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance named *bec* is automatically launched.
|
||||
The `gui` object is the main entry point for interacting with the BEC Widgets framework. It is an instance of the {py:class}`~bec_widgets.cli.client_utils.BECGuiClient` class, which provides methods to create and manage GUI components. Upon BEC startup, a default {py:class}`~bec_widgets.cli.client.BECDockArea` instance named *bec* is automatically launched.
|
||||
|
||||
A launcher interface is available via the top menu bar under New → Open Launcher. This opens a window where users can launch a new [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance, an [AutoUpdate](#user.auto_updates) instance, individual widgets or a custom *ui file* created with *BEC Designer*. Alternatively, users can launch a new [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) from the command line:
|
||||
A launcher interface is available via the top menu bar under New → Open Launcher. This opens a window where users can launch a new {py:class}`~bec_widgets.cli.client.BECDockArea` instance, an [AutoUpdate](#user.auto_updates) instance, individual widgets or a custom *ui file* created with *BEC Designer*. Alternatively, users can launch a new {py:class}`~bec_widgets.cli.client.BECDockArea` from the command line:
|
||||
|
||||
```python
|
||||
dock_area = gui.new() # launches a new BECDockArea instance
|
||||
@@ -19,7 +19,7 @@ If a name is provided, the new dock area will use that name. If the name already
|
||||
|
||||
|
||||
## BECDockArea
|
||||
The [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) is a versatile container for quickly building customized GUIs. It supports adding new widgets either through the CLI or directly via toolbar actions. Widgets must be added into [`BECDock`](/api_reference/_autosummary/bec_widgets.cli.client.BECDock) instances, which serve as the individual containers. These docks can be arranged freely, detached from the main window, and used as floating panels.
|
||||
The {py:class}`~bec_widgets.cli.client.BECDockArea` is a versatile container for quickly building customized GUIs. It supports adding new widgets either through the CLI or directly via toolbar actions. Widgets must be added into {py:class}`~bec_widgets.cli.client.BECDockArea` instances, which serve as the individual containers. These docks can be arranged freely, detached from the main window, and used as floating panels.
|
||||
|
||||
From the CLI, you can create new docks like this:
|
||||
|
||||
@@ -34,23 +34,23 @@ dock = gui.new().new()
|
||||
 -->
|
||||
|
||||
## 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.
|
||||
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. {ref}`user.widgets`). More widgets can be added by the users, and we invite you to explore the {ref}`developer.widgets` to learn how to create custom widgets.
|
||||
For the introduction given here, we will focus on the plotting widgets of BECWidgets.
|
||||
|
||||
<!-- 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 [`WaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.WaveForm) 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.WaveForm.rst#bec_widgets.cli.client.WaveForm.plot) returns the plot object.
|
||||
The {py:class}`~bec_widgets.cli.client.Waveform` 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 {py:meth}`~bec_widgets.cli.client.Waveform.plot` returns the plot object.
|
||||
|
||||
```python
|
||||
plt = gui.new().new().new(gui.available_widgets.Waveform)
|
||||
plt.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 ([`plt.title = 'my title' `](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.title)), axis labels ([`plt.x_label = 'my x label'`](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.x_label))
|
||||
<!-- or limits ([`set_x_lim()`](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.x_lim)). -->
|
||||
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 (`title`), axis labels (`x_label`)
|
||||
<!-- or limits (`x_lim`). -->
|
||||
|
||||
We invite you to explore the API of the WaveForm in the [documentation](user.widgets.waveform_1d) or directly in the command line.
|
||||
We invite you to explore the API of the WaveForm in the {ref}`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.
|
||||
|
||||
@@ -68,18 +68,18 @@ curve = plt.plot(x=[1,2,3,4], y=[1,4,9,16])
|
||||
|
||||
**Scatter Plot**
|
||||
|
||||
The [`WaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.WaveForm) 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).
|
||||
The {py:class}`~bec_widgets.cli.client.Waveform` 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 {ref}`user.widgets.scatter_2d`.
|
||||
|
||||
**Motor Map**
|
||||
|
||||
The [`MotorMap`](/api_reference/_autosummary/bec_widgets.cli.client.MotorMap) 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).
|
||||
The {py:class}`~bec_widgets.cli.client.MotorMap` 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 {ref}`user.widgets.motor_map`.
|
||||
|
||||
**Image Plot**
|
||||
|
||||
The [`Image`](/api_reference/_autosummary/bec_widgets.cli.client.Image) 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).
|
||||
The {py:class}`~bec_widgets.cli.client.Image` 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 {ref}`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.
|
||||
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 {ref}`user.api_reference`, but also by using BEC Widgets, exploring the available functions and check their dockstrings.
|
||||
```python
|
||||
gui.new? # shows the dockstring of the new method
|
||||
```
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
```{tab} Overview
|
||||
|
||||
The [`BECProgressbar`](/api_reference/_autosummary/bec_widgets.cli.client.BECProgressBar) widget is a general purpose progress bar that follows the BEC theme and style. It can be embedded in any application to display the progress of a task or operation.
|
||||
The {py:class}`~bec_widgets.cli.client.BECProgressBar` widget is a general purpose progress bar that follows the BEC theme and style. It can be embedded in any application to display the progress of a task or operation.
|
||||
|
||||
## Key Features:
|
||||
- **Modern Design**: The BEC Progressbar widget is designed with a modern and sleek appearance, following the BEC theme.
|
||||
@@ -35,6 +35,8 @@ pb.set_value(50)
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECProgressBar.rst
|
||||
.. autoclass:: bec_widgets.cli.client.BECProgressBar
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`BEC Status Box`](/api_reference/_autosummary/bec_widgets.cli.client.BECStatusBox) widget is designed to monitor the status and health of all running BEC processes. This widget provides a real-time overview of the BEC core services, including DeviceServer, ScanServer, SciHub, ScanBundler, and FileWriter. The top-level display indicates the overall state of the BEC services, while the collapsed view allows users to delve into the status of each individual process. By double-clicking on a specific process, users can access a detailed popup window with live updates of the metrics for that process.
|
||||
The {py:class}`~bec_widgets.cli.client.BECStatusBox` widget is designed to monitor the status and health of all running BEC processes. This widget provides a real-time overview of the BEC core services, including DeviceServer, ScanServer, SciHub, ScanBundler, and FileWriter. The top-level display indicates the overall state of the BEC services, while the collapsed view allows users to delve into the status of each individual process. By double-clicking on a specific process, users can access a detailed popup window with live updates of the metrics for that process.
|
||||
|
||||
## Key Features:
|
||||
- **Comprehensive Service Monitoring**: Track the state of individual BEC services, including real-time updates on their health and status.
|
||||
@@ -33,6 +33,8 @@ Once the `BECStatusBox` is added, users can interact with it to view the status
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECStatusBox.rst
|
||||
.. autoclass:: bec_widgets.cli.client.BECStatusBox
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -146,8 +146,14 @@ my_gui.show()
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.DarkModeButton.rst
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.ColorButton.rst
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.ColormapSelector.rst
|
||||
.. autoclass:: bec_widgets.cli.client.DarkModeButton
|
||||
:members:
|
||||
:show-inheritance:
|
||||
.. autoclass:: bec_widgets.cli.client.ColorButton
|
||||
:members:
|
||||
:show-inheritance:
|
||||
.. autoclass:: bec_widgets.cli.client.ColormapSelector
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -39,7 +39,7 @@ The `Reset Button` is used to reset the scan queue. It prompts the user for conf
|
||||
- **Toolbar and Button Options**: Can be configured as a toolbar button or a standard push button.
|
||||
```
|
||||
|
||||
`````{tab} Examples
|
||||
````{tab} Examples
|
||||
|
||||
Integrating these buttons into a BEC GUI layout is straightforward. The following examples demonstrate how to embed these buttons within a custom GUI layout using `QtWidgets`.
|
||||
|
||||
@@ -66,12 +66,21 @@ app.exec_()
|
||||
```
|
||||
|
||||
`ResumeButton`, `ResetButton`, and `AbortButton` may be used in an exactly analogous way.
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.StopButton.rst
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.ResumeButton.rst
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.AbortButton.rst
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.ResetButton.rst
|
||||
.. autoclass:: bec_widgets.cli.client.StopButton
|
||||
:members:
|
||||
:show-inheritance:
|
||||
.. autoclass:: bec_widgets.cli.client.ResumeButton
|
||||
:members:
|
||||
:show-inheritance:
|
||||
.. autoclass:: bec_widgets.cli.client.AbortButton
|
||||
:members:
|
||||
:show-inheritance:
|
||||
.. autoclass:: bec_widgets.cli.client.ResetButton
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
`````
|
||||
````
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`DAPComboBox`](/api_reference/_autosummary/bec_widgets.widgets.dap_combo_box.dap_combo_box.DAPComboBox) is a widget that extends the functionality of a standard `QComboBox` to allow the user to select a DAP process from all available DAP models.
|
||||
One of its signals `new_dap_config` is designed to be connected to the [`add_dap(str, str, str)`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.rst#bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.add_dap) slot from the BECWaveformWidget to add a DAP process.
|
||||
The {py:class}`~bec_widgets.widgets.dap_combo_box.dap_combo_box.DAPComboBox` is a widget that extends the functionality of a standard `QComboBox` to allow the user to select a DAP process from all available DAP models.
|
||||
One of its signals `new_dap_config` is designed to be connected to the {py:class}`~bec_widgets.widgets.plots.waveform.waveform.Waveform.add_dap_curve` slot from the Waveform widget to add a DAP process.
|
||||
|
||||
## Key Features:
|
||||
- **Select DAP model**: Select one of the available DAP models.
|
||||
@@ -30,11 +30,6 @@ The following slots are available for the `DAP ComboBox` widget:
|
||||
- `select_y_axis(str)` : Slot to select the current y axis, emits the `x_axis_updated` signal
|
||||
- `select_fit_model(str)` : Slot to select the current fit model, emits the `fit_model_updated` signal. If x and y axis are set, it will also emit the `new_dap_config` signal.
|
||||
````
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.widgets.dap_combo_box.dap_combo_box.DAPCombobox.rst
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`Device Browser`](/api_reference/_autosummary/bec_widgets.cli.client.DeviceBrowser) widget provides a user-friendly interface for browsing through all available devices in the current BEC session. As it supports drag functionality, users can easily drag and drop device into other widgets or applications.
|
||||
The {py:class}`~bec_widgets.cli.client.DeviceBrowser` widget provides a user-friendly interface for browsing through all available devices in the current BEC session. As it supports drag functionality, users can easily drag and drop device into other widgets or applications.
|
||||
|
||||
```{note}
|
||||
The `Device Browser` widget is currently under development. Other widgets may not support drag and drop functionality yet.
|
||||
@@ -34,6 +34,8 @@ dock_area.device_browser.DeviceBrowser
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.DeviceBrowser.rst
|
||||
.. autoclass:: bec_widgets.cli.client.DeviceBrowser
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
@@ -114,12 +114,16 @@ The following Qt properties are also included:
|
||||
|
||||
````{tab} API - ComboBox
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.DeviceComboBox.rst
|
||||
.. autoclass:: bec_widgets.cli.client.DeviceComboBox
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
````{tab} API - LineEdit
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.DeviceLineEdit.rst
|
||||
.. autoclass:: bec_widgets.cli.client.DeviceLineEdit
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
[`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) is a powerful and flexible container designed to host various widgets and docks within a grid layout. It provides an environment for organizing and managing complex user interfaces, making it ideal for applications that require multiple tools and data visualizations to be displayed simultaneously. BECDockArea is particularly useful for embedding not only visualization tools but also other interactive components, allowing users to tailor their workspace to their specific needs.
|
||||
`BECDockArea` is a powerful and flexible container designed to host various widgets and docks within a grid layout. It provides an environment for organizing and managing complex user interfaces, making it ideal for applications that require multiple tools and data visualizations to be displayed simultaneously. BECDockArea is particularly useful for embedding not only visualization tools but also other interactive components, allowing users to tailor their workspace to their specific needs.
|
||||
|
||||
- **Flexible Dock Management**: Easily add, remove, and rearrange docks within `BECDockArea`, providing a customized layout for different tasks.
|
||||
- **State Persistence**: Save and restore the state of the dock area, enabling consistent user experiences across sessions.
|
||||
- **Dock Customization**: Add docks with customizable positions, names, and behaviors, such as floating or closable docks.
|
||||
- **Integration with Widgets**: Integrate various widgets like [`WaveformWidget`](user.widgets.waveform_widget), [`ImageWidget`](user.widgets.image_widget), and [`MotorMapWidget`](user.widgets.motor_map) into [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea), either as standalone tools or as part of a more complex interface.
|
||||
- **Integration with Widgets**: Integrate various widgets like [`WaveformWidget`](user.widgets.waveform_widget), [`ImageWidget`](user.widgets.image_widget), and [`MotorMapWidget`](user.widgets.motor_map) into `BECDockArea`, either as standalone tools or as part of a more complex interface.
|
||||
|
||||
**BEC Dock Area Components Schema**
|
||||
|
||||
@@ -114,7 +114,9 @@ When removing a dock, all widgets within the dock will be removed as well. This
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECDockArea.rst
|
||||
.. autoclass:: bec_widgets.cli.client.BECDockArea
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
@@ -101,6 +101,8 @@ heatmap_widget.v_max = 1000
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.widgets.plots.heatmap.heatmap.Heatmap.rst
|
||||
.. autoclass:: bec_widgets.widgets.plots.heatmap.heatmap.Heatmap
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
@@ -105,6 +105,8 @@ Since the Image Widget does not have prior information about the shape of incomi
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.Image.rst
|
||||
.. autoclass:: bec_widgets.cli.client.Image
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`LMFit Dialog`](/api_reference/_autosummary/bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog.LMFitDialog) is a widget that is developed to be used together with the [`Waveform`](/api_reference/_autosummary/bec_widgets.widgets.plots.waveform.waveform.Waveform) widget. The `Waveform` widget allows user to submit a fit request to BEC's [DAP server](https://bec.readthedocs.io/en/latest/developer/getting_started/architecture.html) choosing from a selection of [LMFit models](https://lmfit.github.io/lmfit-py/builtin_models.html#) to fit monitored data sources. The `LMFit Dialog` provides an interface to monitor these fits, including statistics and fit parameters in real time.
|
||||
Within the `Waveform` widget, the dialog is accessible via the toolbar and will be automatically linked to the current waveform widget. For a more customised use, we can embed the `LMFit Dialog` in a larger GUI using the *BEC Designer*. In this case, one has to connect the [`update_summary_tree`](/api_reference/_autosummary/bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog.LMFitDialog.rst#bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.update_summary_tree) slot of the LMFit Dialog to the [`dap_summary_update`](/api_reference/_autosummary/bec_widgets.widgets.plots.waveform.waveform_widget.Waveform.rst#bec_widgets.widgets.plots.waveform.waveform.Waveform.dap_summary_update) signal of the Waveform widget to ensure its functionality.
|
||||
The `LMFitDialog` is a widget that is developed to be used together with the `Waveform` widget. The `Waveform` widget allows user to submit a fit request to BEC's [DAP server](https://bec.readthedocs.io/en/latest/developer/getting_started/architecture.html) choosing from a selection of [LMFit models](https://lmfit.github.io/lmfit-py/builtin_models.html#) to fit monitored data sources. The `LMFit Dialog` provides an interface to monitor these fits, including statistics and fit parameters in real time.
|
||||
Within the `Waveform` widget, the dialog is accessible via the toolbar and will be automatically linked to the current waveform widget. For a more customised use, we can embed the `LMFit Dialog` in a larger GUI using the *BEC Designer*. In this case, one has to connect the `update_summary_tree` slot of the LMFit Dialog to the `dap_summary_update` signal of the Waveform widget to ensure its functionality.
|
||||
|
||||
|
||||
## Key Features:
|
||||
@@ -34,11 +34,7 @@ waveform.dap_summary_update.connect(lmfit_dialog.update_summary_tree)
|
||||
|
||||
```
|
||||
````
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.rst
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ mm1.map(x_name='aptrx', y_name='aptry')
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.MotorMap.rst
|
||||
.. autoclass:: bec_widgets.cli.client.MotorMap
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
@@ -89,6 +89,8 @@ multi_waveform.export_to_matplotlib()
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.MultiWaveform.rst
|
||||
.. autoclass:: bec_widgets.cli.client.MultiWaveform
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`PositionIndicator`](/api_reference/_autosummary/bec_widgets.cli.client.PositionIndicator) widget is a simple yet effective tool for visually indicating the position of a motor within its set limits. This widget is particularly useful in applications where it is important to provide a visual clue of the motor's current position relative to its minimum and maximum values. The `PositionIndicator` can be easily integrated into your GUI application either through direct code instantiation or by using `BEC Designer`.
|
||||
The `PositionIndicator` widget is a simple yet effective tool for visually indicating the position of a motor within its set limits. This widget is particularly useful in applications where it is important to provide a visual clue of the motor's current position relative to its minimum and maximum values. The `PositionIndicator` can be easily integrated into your GUI application either through direct code instantiation or by using `BEC Designer`.
|
||||
|
||||
## Key Features:
|
||||
- **Position Visualization**: Displays the current position of a motor on a linear scale, showing its location relative to the defined limits.
|
||||
@@ -36,7 +36,7 @@ Within the BEC Designer's [property editor](https://doc.qt.io/qt-6/designer-widg
|
||||
|
||||
````{tab} Examples
|
||||
|
||||
The `PositionIndicator` widget can be embedded in a [`BECDockArea`](#user.widgets.bec_dock_area) or used as an individual component in your application through `BEC Designer`. Below are examples demonstrating how to create and use the `PositionIndicator` from the CLI and also directly within Code.
|
||||
The `PositionIndicator` widget can be embedded in a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BEC Designer`. Below are examples demonstrating how to create and use the `PositionIndicator` from the CLI and also directly within Code.
|
||||
|
||||
## Example 1 - Creating a Position Indicator in Code
|
||||
|
||||
@@ -95,6 +95,8 @@ self.position_indicator.set_value(new_position_value)
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.PositionIndicator.rst
|
||||
.. autoclass:: bec_widgets.cli.client.PositionIndicator
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`PositionerBox`](/api_reference/_autosummary/bec_widgets.cli.client.PositionerBox) widget provides a graphical user interface to control a positioner device within the BEC environment. This widget allows users to interact with a positioner by setting setpoints, tweaking the motor position, and stopping motion. The device selection can be done via a small button under the device label, through `BEC Designer`, or by using the command line interface (CLI). This flexibility makes the `PositionerBox` an essential tool for tasks involving precise position control.
|
||||
The `PositionerBox` widget provides a graphical user interface to control a positioner device within the BEC environment. This widget allows users to interact with a positioner by setting setpoints, tweaking the motor position, and stopping motion. The device selection can be done via a small button under the device label, through `BEC Designer`, or by using the command line interface (CLI). This flexibility makes the `PositionerBox` an essential tool for tasks involving precise position control.
|
||||
|
||||
## Key Features:
|
||||
- **Device Selection**: Easily select a positioner device by clicking the button under the device label or by configuring the widget in `BEC Designer`.
|
||||
@@ -58,6 +58,8 @@ self.positioner_box.set_positioner("motor2")
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.PositionerBox.rst
|
||||
.. autoclass:: bec_widgets.cli.client.PositionerBox
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`PositionerBox2D`](/api_reference/_autosummary/bec_widgets.cli.client.PositionerBox2D) widget is very similar to the [`PositionerBox`](/user/widgets/positioner_box/positioner_box) but allows controlling two positioners at the same time, in a horizontal and vertical orientation respectively. It is intended primarily for controlling axes which have a perpendicular relationship like that. In other cases, it may be better to use a `PositionerGroup` instead.
|
||||
The `PositionerBox2D` widget is very similar to the `PositionerBox` but allows controlling two positioners at the same time, in a horizontal and vertical orientation respectively. It is intended primarily for controlling axes which have a perpendicular relationship like that. In other cases, it may be better to use a `PositionerGroup` instead.
|
||||
|
||||
The `PositionerBox2D` has the same features as the standard `PositionerBox`, but additionally, step buttons which move the positioner by the selected step size, and tweak buttons which move by a tenth of the selected step size.
|
||||
|
||||
@@ -55,6 +55,8 @@ self.positioner_box.set_positioner_verr("samy")
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.PositionerBox2D.rst
|
||||
.. autoclass:: bec_widgets.cli.client.PositionerBox2D
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`Ring Progress Bar`](/api_reference/_autosummary/bec_widgets.cli.client.RingProgressBar) widget is a circular progress bar designed to visualize the progress of tasks in a clear and intuitive manner. This widget is particularly useful in applications where task progress needs to be represented as a percentage. The `Ring Progress Bar` can be controlled directly via its API or can be hooked up to track the progress of a device readback or scan, providing real-time visual feedback.
|
||||
The `RingProgressBar` widget is a circular progress bar designed to visualize the progress of tasks in a clear and intuitive manner. This widget is particularly useful in applications where task progress needs to be represented as a percentage. The `Ring Progress Bar` can be controlled directly via its API or can be hooked up to track the progress of a device readback or scan, providing real-time visual feedback.
|
||||
|
||||
## Key Features:
|
||||
- **Circular Progress Visualization**: Displays a circular progress bar to represent task completion.
|
||||
@@ -98,7 +98,9 @@ progress.set_value([50, 75, 25])
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.RingProgressBar.rst
|
||||
.. autoclass:: bec_widgets.cli.client.RingProgressBar
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`BEC Queue`](/api_reference/_autosummary/bec_widgets.cli.client.BECQueue) widget provides a real-time display and control of the BEC scan queue, allowing users to monitor, manage, and control the status of ongoing and pending scans. The widget automatically updates to reflect the current state of the scan queue, displaying critical information such as scan numbers, types, and statuses. Additionally, it provides control options to stop individual scans, stop the entire queue, resume, and reset the queue, making it a powerful tool for managing scan operations in the BEC environment.
|
||||
The `BECQueue` widget provides a real-time display and control of the BEC scan queue, allowing users to monitor, manage, and control the status of ongoing and pending scans. The widget automatically updates to reflect the current state of the scan queue, displaying critical information such as scan numbers, types, and statuses. Additionally, it provides control options to stop individual scans, stop the entire queue, resume, and reset the queue, making it a powerful tool for managing scan operations in the BEC environment.
|
||||
|
||||
## Key Features:
|
||||
- **Real-Time Queue Monitoring**: Displays the current state of the BEC scan queue, with automatic updates as the queue changes.
|
||||
@@ -39,6 +39,8 @@ Once the widget is added, it will automatically display the current scan queue
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECQueue.rst
|
||||
.. autoclass:: bec_widgets.cli.client.BECQueue
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`Scan Control`](/api_reference/_autosummary/bec_widgets.cli.client.ScanControl) widget provides a graphical user interface (GUI) to manage various scan operations in a BEC environment. It is designed to interact with the BEC server, enabling users to start and stop scans. The widget automatically creates the necessary input form based on the scan's signature and gui_config, making it highly adaptable to different scanning processes.
|
||||
The `ScanControl` widget provides a graphical user interface (GUI) to manage various scan operations in a BEC environment. It is designed to interact with the BEC server, enabling users to start and stop scans. The widget automatically creates the necessary input form based on the scan's signature and gui_config, making it highly adaptable to different scanning processes.
|
||||
|
||||
## Key Features:
|
||||
- **Automatic Interface Generation**: Automatically generates a control interface based on scan signatures and `gui_config`.
|
||||
@@ -59,6 +59,8 @@ scan_control = dock_area.new().new(gui.available_widgets.ScanControl)
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.ScanControl.rst
|
||||
.. autoclass:: bec_widgets.cli.client.ScanControl
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -34,6 +34,8 @@ The ScatterWaveform widget only plots the data points if both x and y axis motor
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.ScatterWaveform.rst
|
||||
.. autoclass:: bec_widgets.cli.client.ScatterWaveform
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
@@ -104,14 +104,6 @@ The following Qt properties are also included:
|
||||
|
||||
````
|
||||
|
||||
````{tab} API - ComboBox
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.control.device_input.signal_combobox.SignalComboBox.rst
|
||||
```
|
||||
````
|
||||
|
||||
````{tab} API - LineEdit
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.control.device_input.signal_line_edit.SignalLineEdit.rst
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`SignalLabel`](/api_reference/_autosummary/bec_widgets.cli.client.SignalLabel) displays the value of a signal from a device, with optional customization for labels, units, decimal formatting, and signal selection. It is designed for use in BEC (Beamline Experiment Control) GUIs to monitor values which beamline operators might want to keep an eye on, e.g. sample position, flux, hutch state...
|
||||
The `SignalLabel` displays the value of a signal from a device, with optional customization for labels, units, decimal formatting, and signal selection. It is designed for use in BEC (Beamline Experiment Control) GUIs to monitor values which beamline operators might want to keep an eye on, e.g. sample position, flux, hutch state...
|
||||
|
||||
## Key Features:
|
||||
- Display: Shows the current value of a device signal.
|
||||
@@ -88,7 +88,9 @@ The various properties can also be set when the SignalLabel widget is added to a
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.TextBox.rst
|
||||
.. autoclass:: bec_widgets.cli.client.TextBox
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`SpinnerWidget`](/api_reference/_autosummary/bec_widgets.utility.spinner.spinner.SpinnerWidget) is a simple and versatile widget designed to indicate loading or movement within an application. It is commonly used to show that a device is in motion or that an operation is ongoing. The `SpinnerWidget` can be easily integrated into your GUI application either through direct code instantiation or by using `BEC Designer`.
|
||||
The `SpinnerWidget` is a simple and versatile widget designed to indicate loading or movement within an application. It is commonly used to show that a device is in motion or that an operation is ongoing. The `SpinnerWidget` can be easily integrated into your GUI application either through direct code instantiation or by using `BEC Designer`.
|
||||
|
||||
## Key Features:
|
||||
- **Loading Indicator**: Provides a visual indication of ongoing operations or device movement.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`Text Box Widget`](/api_reference/_autosummary/bec_widgets.cli.client.TextBox) is a versatile widget that allows users to display text within the BEC GUI. It supports both plain text and HTML, making it useful for displaying simple messages or more complex formatted content. This widget is particularly suited for integrating textual content directly into the user interface, whether as a standalone message box or as part of a larger application interface.
|
||||
The {py:class}`~bec_widgets.cli.client.TextBox` is a versatile widget that allows users to display text within the BEC GUI. It supports both plain text and HTML, making it useful for displaying simple messages or more complex formatted content. This widget is particularly suited for integrating textual content directly into the user interface, whether as a standalone message box or as part of a larger application interface.
|
||||
|
||||
## Key Features:
|
||||
- **Text Display**: Display either plain text or HTML content, with automatic detection of the format.
|
||||
@@ -45,7 +45,9 @@ text_box.set_html_text("<h1>Welcome to BEC Widgets</h1><p>This is an example of
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.TextBox.rst
|
||||
.. autoclass:: bec_widgets.cli.client.TextBox
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`Toggle Switch`](/api_reference/_autosummary/bec_widgets.cli.client.ToggleSwitch) widget provides a simple, customizable toggle switch that can be used to represent binary states (e.g., on/off, true/false) within a GUI. This widget is designed to be used directly in code or added through `BEC Designer`, making it versatile for various applications where a user-friendly switch is needed.
|
||||
The {py:class}`~bec_widgets.cli.client.ToggleSwitch` widget provides a simple, customizable toggle switch that can be used to represent binary states (e.g., on/off, true/false) within a GUI. This widget is designed to be used directly in code or added through `BEC Designer`, making it versatile for various applications where a user-friendly switch is needed.
|
||||
|
||||
## Key Features:
|
||||
- **Binary State Representation**: Represents a simple on/off state with a smooth toggle animation.
|
||||
|
||||
@@ -101,6 +101,8 @@ print(dap_bpm3a.dap_params)
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst
|
||||
.. autoclass:: bec_widgets.cli.client.Waveform
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`Website Widget`](/api_reference/_autosummary/bec_widgets.cli.client.WebsiteWidget) is a versatile tool that allows users to display websites directly within the BEC GUI. This widget is useful for embedding documentation, dashboards, or any web-based tools within the application interface. It is designed to be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BEC Designer`.
|
||||
The {py:class}`~bec_widgets.cli.client.WebsiteWidget` is a versatile tool that allows users to display websites directly within the BEC GUI. This widget is useful for embedding documentation, dashboards, or any web-based tools within the application interface. It is designed to be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BEC Designer`.
|
||||
|
||||
## Key Features:
|
||||
- **URL Display**: Set and display any website URL within the widget.
|
||||
@@ -66,6 +66,8 @@ print(f"The current URL is: {current_url}")
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.WebsiteWidget.rst
|
||||
.. autoclass:: bec_widgets.cli.client.WebsiteWidget
|
||||
:members:
|
||||
:show-inheritance:
|
||||
```
|
||||
````
|
||||
|
||||
@@ -4,9 +4,9 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "2.38.0"
|
||||
version = "2.40.0"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Programming Language :: Python :: 3",
|
||||
@@ -14,7 +14,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"bec_ipython_client~=3.52", # needed for jupyter console
|
||||
"bec_lib~=3.52",
|
||||
"bec_lib~=3.68",
|
||||
"bec_qthemes~=0.7, >=0.7",
|
||||
"black~=25.0", # needed for bw-generate-cli
|
||||
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
||||
@@ -24,6 +24,7 @@ dependencies = [
|
||||
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
||||
"qtpy~=2.4",
|
||||
"qtmonaco~=0.5",
|
||||
"thefuzz~=0.22",
|
||||
]
|
||||
|
||||
|
||||
@@ -41,7 +42,7 @@ dev = [
|
||||
"pytest-cov~=6.1.1",
|
||||
"watchdog~=6.0",
|
||||
"pre_commit~=4.2",
|
||||
"thefuzz~=0.22",
|
||||
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -286,3 +286,85 @@ def test_waveform_passing_device(qtbot, bec_client_lib, connected_client_gui_obj
|
||||
# check plotted data
|
||||
x_data, y_data = c1.get_data()
|
||||
assert np.array_equal(y_data, last_scan_data.devices.samx.samx_setpoint.read().get("value"))
|
||||
|
||||
|
||||
@pytest.mark.timeout(120)
|
||||
@pytest.mark.parametrize(
|
||||
"history_selector", ["scan_id", "scan_number"]
|
||||
) # ensure unique curves per run
|
||||
def test_rpc_waveform_history_curve(
|
||||
qtbot, bec_client_lib, connected_client_gui_obj, history_selector
|
||||
):
|
||||
"""
|
||||
E2E test for the new history curve feature:
|
||||
- Run 3 scans
|
||||
- For each scan, fetch history curve data using either scan_id OR scan_number (parametrized)
|
||||
- Compare waveform data with BEC client scan data
|
||||
Note: Parameterization prevents adding the same logical curve twice (which would collide on label).
|
||||
"""
|
||||
gui = connected_client_gui_obj
|
||||
dock = gui.bec
|
||||
client = bec_client_lib
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
queue = client.queue
|
||||
|
||||
wf = dock.new("wf_dock").new("Waveform")
|
||||
|
||||
# Collect references for validation
|
||||
scan_meta = [] # list of dicts with scan_id, scan_number, data
|
||||
|
||||
# Run 3 scans and collect their metadata and data
|
||||
for i in range(3):
|
||||
status = scans.line_scan(dev.samx, -5 + i, 5 + i, steps=10, exp_time=0.01, relative=False)
|
||||
status.wait()
|
||||
|
||||
# Wait until the history entry appears and corresponds to this scan
|
||||
def _wait_for_scan_in_history():
|
||||
if len(client.history) == 0:
|
||||
return False
|
||||
return client.history[-1].metadata.bec.get("scan_id", None) == status.scan.scan_id
|
||||
|
||||
qtbot.waitUntil(_wait_for_scan_in_history, timeout=10000)
|
||||
|
||||
hist_item = client.history[-1]
|
||||
item = queue.scan_storage.storage[-1]
|
||||
data = item.live_data if hasattr(item, "live_data") else item.data
|
||||
scan_meta.append(
|
||||
{
|
||||
"scan_id": hist_item.metadata.bec.get("scan_id"),
|
||||
"scan_number": hist_item.metadata.bec.get("scan_number"),
|
||||
"data": data,
|
||||
}
|
||||
)
|
||||
|
||||
# For each scan, fetch history curve by the chosen selector and compare to client data
|
||||
for meta in scan_meta:
|
||||
sel_value = meta[history_selector]
|
||||
scan_data = meta["data"]
|
||||
|
||||
# Add curve from history using the chosen selector; single curve per scan to avoid duplicates
|
||||
kwargs = {history_selector: sel_value}
|
||||
curve = wf.plot(x_name="samx", y_name="bpm4i", **kwargs)
|
||||
|
||||
num_elements = 10
|
||||
|
||||
# Wait until curve has the expected number of points
|
||||
def _curve_ready():
|
||||
try:
|
||||
x, y = curve.get_data()
|
||||
except Exception:
|
||||
return False
|
||||
return x is not None and len(x) == num_elements and len(y) == num_elements
|
||||
|
||||
qtbot.waitUntil(_curve_ready, timeout=10000)
|
||||
|
||||
# Get plotted data
|
||||
x_vals, y_vals = curve.get_data()
|
||||
|
||||
# Compare against BEC client scan data
|
||||
np.testing.assert_equal(x_vals, np.array(scan_data["samx"]["samx"].val))
|
||||
np.testing.assert_equal(y_vals, np.array(scan_data["bpm4i"]["bpm4i"].val))
|
||||
|
||||
# Clean up
|
||||
curve.remove()
|
||||
|
||||
@@ -371,6 +371,13 @@ def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_fro
|
||||
) # Get last image from Redis monitor 2D endpoint
|
||||
assert np.allclose(img.get_data(), last_img)
|
||||
|
||||
# Now add a device with a preview signal
|
||||
img = widget.image(["eiger", "preview"])
|
||||
s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
|
||||
s.wait()
|
||||
|
||||
qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
|
||||
|
||||
# Test removing the widget, or leaving it open for the next test
|
||||
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||
|
||||
@@ -577,6 +584,13 @@ def test_widgets_e2e_ring_progress_bar(qtbot, connected_client_gui_obj, random_g
|
||||
dock: client.BECDock
|
||||
widget: client.RingProgressBar
|
||||
|
||||
widget.set_number_of_bars(3)
|
||||
widget.rings[0].set_update("manual")
|
||||
widget.rings[0].set_value(30)
|
||||
widget.rings[0].set_min_max_values(0, 100)
|
||||
widget.rings[1].set_update("scan")
|
||||
widget.rings[2].set_update("device", device="samx")
|
||||
|
||||
# Test rpc calls
|
||||
dev = bec.device_manager.devices
|
||||
scans = bec.scans
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
from bec_lib.bec_service import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.redis_connector import RedisConnector
|
||||
from bec_lib.scan_history import ScanHistory
|
||||
|
||||
from bec_widgets.tests.utils import DEVICES, DMMock, FakePositioner, Positioner
|
||||
|
||||
@@ -238,3 +239,18 @@ def create_dummy_scan_item():
|
||||
"scan_report_devices": ["samx"],
|
||||
}
|
||||
return dummy_scan
|
||||
|
||||
|
||||
def inject_scan_history(widget, scan_history_factory, *history_args):
|
||||
"""
|
||||
Helper to inject scan history messages into client history.
|
||||
"""
|
||||
history_msgs = []
|
||||
for scan_id, scan_number in history_args:
|
||||
history_msgs.append(scan_history_factory(scan_id=scan_id, scan_number=scan_number))
|
||||
widget.client.history = ScanHistory(widget.client, False)
|
||||
for msg in history_msgs:
|
||||
widget.client.history._scan_data[msg.scan_id] = msg
|
||||
widget.client.history._scan_ids.append(msg.scan_id)
|
||||
widget.client.queue.scan_storage.current_scan = None
|
||||
return history_msgs
|
||||
|
||||
@@ -5,8 +5,9 @@ import h5py
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_lib import messages
|
||||
from bec_lib.messages import _StoredDataInfo
|
||||
from pytestqt.exceptions import TimeoutError as QtBotTimeoutError
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from qtpy.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
|
||||
@@ -69,6 +70,14 @@ def clean_singleton():
|
||||
error_popups._popup_utility_instance = None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def suppress_message_box(monkeypatch):
|
||||
"""
|
||||
Auto-suppress any QMessageBox.exec_ calls by returning Ok immediately.
|
||||
"""
|
||||
monkeypatch.setattr(QMessageBox, "exec_", lambda *args, **kwargs: QMessageBox.Ok)
|
||||
|
||||
|
||||
def create_widget(qtbot, widget, *args, **kwargs):
|
||||
"""
|
||||
Create a widget and add it to the qtbot for testing. This is a helper function that
|
||||
@@ -115,9 +124,25 @@ def create_history_file(file_path, data: dict, metadata: dict) -> messages.ScanH
|
||||
elif isinstance(sub_value, dict):
|
||||
for sub_sub_key, sub_sub_value in sub_value.items():
|
||||
sub_sub_group = metadata_bec[key].create_group(sub_key)
|
||||
# Handle _StoredDataInfo objects
|
||||
if isinstance(sub_sub_value, _StoredDataInfo):
|
||||
# Store the numeric shape
|
||||
sub_sub_group.create_dataset("shape", data=sub_sub_value.shape)
|
||||
# Store the dtype as a UTF-8 string
|
||||
dt = sub_sub_value.dtype or ""
|
||||
sub_sub_group.create_dataset(
|
||||
"dtype", data=dt, dtype=h5py.string_dtype(encoding="utf-8")
|
||||
)
|
||||
continue
|
||||
if isinstance(sub_sub_value, list):
|
||||
sub_sub_value = json.dumps(sub_sub_value)
|
||||
sub_sub_group.create_dataset(sub_sub_key, data=sub_sub_value)
|
||||
json_val = json.dumps(sub_sub_value)
|
||||
sub_sub_group.create_dataset(sub_sub_key, data=json_val)
|
||||
elif isinstance(sub_sub_value, dict):
|
||||
for k2, v2 in sub_sub_value.items():
|
||||
val = json.dumps(v2) if isinstance(v2, list) else v2
|
||||
sub_sub_group.create_dataset(k2, data=val)
|
||||
else:
|
||||
sub_sub_group.create_dataset(sub_sub_key, data=sub_sub_value)
|
||||
else:
|
||||
metadata_bec[key].create_dataset(sub_key, data=sub_value)
|
||||
else:
|
||||
@@ -144,6 +169,8 @@ def create_history_file(file_path, data: dict, metadata: dict) -> messages.ScanH
|
||||
end_time=time.time(),
|
||||
num_points=metadata["num_points"],
|
||||
request_inputs=metadata["request_inputs"],
|
||||
stored_data_info=metadata.get("stored_data_info"),
|
||||
metadata={"scan_report_devices": metadata.get("scan_report_devices")},
|
||||
)
|
||||
return msg
|
||||
|
||||
@@ -194,3 +221,102 @@ def grid_scan_history_msg(tmpdir):
|
||||
|
||||
file_path = str(tmpdir.join("scan_1.h5"))
|
||||
return create_history_file(file_path, data, metadata)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scan_history_factory(tmpdir):
|
||||
"""
|
||||
Factory to create scan history messages with custom parameters.
|
||||
Usage:
|
||||
msg1 = scan_history_factory(scan_id="id1", scan_number=1, num_points=10)
|
||||
msg2 = scan_history_factory(scan_id="id2", scan_number=2, scan_name="grid_scan", num_points=16)
|
||||
"""
|
||||
|
||||
def _factory(
|
||||
scan_id: str = "test_scan",
|
||||
scan_number: int = 1,
|
||||
dataset_number: int = 1,
|
||||
scan_name: str = "line_scan",
|
||||
scan_type: str = "step",
|
||||
num_points: int = 10,
|
||||
x_range: tuple = (-5, 5),
|
||||
y_range: tuple = (-5, 5),
|
||||
):
|
||||
# Generate positions based on scan type
|
||||
if scan_name == "grid_scan":
|
||||
grid_size = int(np.sqrt(num_points))
|
||||
x_grid, y_grid = np.meshgrid(
|
||||
np.linspace(x_range[0], x_range[1], grid_size),
|
||||
np.linspace(y_range[0], y_range[1], grid_size),
|
||||
)
|
||||
x_flat = x_grid.T.ravel()
|
||||
y_flat = y_grid.T.ravel()
|
||||
else:
|
||||
x_flat = np.linspace(x_range[0], x_range[1], num_points)
|
||||
y_flat = np.linspace(y_range[0], y_range[1], num_points)
|
||||
positions = np.vstack((x_flat, y_flat)).T
|
||||
num_pts = len(positions)
|
||||
# Create dummy data
|
||||
data = {
|
||||
"baseline": {"bpm1a": {"bpm1a": {"value": [1], "timestamp": [100]}}},
|
||||
"monitored": {
|
||||
"bpm4i": {
|
||||
"bpm4i": {
|
||||
"value": np.random.rand(num_points),
|
||||
"timestamp": np.random.rand(num_points),
|
||||
}
|
||||
},
|
||||
"bpm3a": {
|
||||
"bpm3a": {
|
||||
"value": np.random.rand(num_points),
|
||||
"timestamp": np.random.rand(num_points),
|
||||
}
|
||||
},
|
||||
"samx": {"samx": {"value": x_flat, "timestamp": np.arange(num_pts)}},
|
||||
"samy": {"samy": {"value": y_flat, "timestamp": np.arange(num_pts)}},
|
||||
},
|
||||
"async": {
|
||||
"async_device": {
|
||||
"async_device": {
|
||||
"value": np.random.rand(num_pts * 10),
|
||||
"timestamp": np.random.rand(num_pts * 10),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
metadata = {
|
||||
"scan_id": scan_id,
|
||||
"scan_name": scan_name,
|
||||
"scan_type": scan_type,
|
||||
"exit_status": "closed",
|
||||
"scan_number": scan_number,
|
||||
"dataset_number": dataset_number,
|
||||
"request_inputs": {
|
||||
"arg_bundle": [
|
||||
"samx",
|
||||
x_range[0],
|
||||
x_range[1],
|
||||
num_pts,
|
||||
"samy",
|
||||
y_range[0],
|
||||
y_range[1],
|
||||
num_pts,
|
||||
],
|
||||
"kwargs": {"relative": True},
|
||||
},
|
||||
"positions": positions.tolist(),
|
||||
"num_points": num_pts,
|
||||
"stored_data_info": {
|
||||
"samx": {"samx": _StoredDataInfo(shape=(num_points,), dtype="float64")},
|
||||
"samy": {"samy": _StoredDataInfo(shape=(num_points,), dtype="float64")},
|
||||
"bpm4i": {"bpm4i": _StoredDataInfo(shape=(10,), dtype="float64")},
|
||||
"async_device": {
|
||||
"async_device": _StoredDataInfo(shape=(num_points * 10,), dtype="float64")
|
||||
},
|
||||
},
|
||||
"scan_report_devices": [b"samx"],
|
||||
}
|
||||
file_path = str(tmpdir.join(f"{scan_id}.h5"))
|
||||
return create_history_file(file_path, data, metadata)
|
||||
|
||||
return _factory
|
||||
|
||||
@@ -28,8 +28,7 @@ def test_plugins_dont_clobber_client_globals(bec_logger: MagicMock):
|
||||
bec_logger.logger.warning.assert_called_with(
|
||||
"Plugin widget Widgets from namespace(Widgets=<class 'tests.unit_tests.test_client_plugin_widgets._TestGlobalPlugin'>) conflicts with a built-in class!"
|
||||
)
|
||||
if sys.version_info >= (3, 11): # No EnumType in python3.10
|
||||
assert isinstance(client.Widgets, enum.EnumType)
|
||||
assert isinstance(client.Widgets, enum.EnumType)
|
||||
|
||||
|
||||
class _TestDuplicatePlugin(RPCBase): ...
|
||||
|
||||
@@ -6,6 +6,10 @@ from qtpy.QtGui import QTransform
|
||||
|
||||
from bec_widgets.utils import Crosshair
|
||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
# pylint: disable = redefined-outer-name
|
||||
|
||||
@@ -363,3 +367,27 @@ def test_get_transformed_position_with_scale(plot_widget_with_crosshair):
|
||||
# Check that the results match expectations
|
||||
assert row == expected_row
|
||||
assert col == expected_col
|
||||
|
||||
|
||||
def test_ignore_invisible_curves_on_move(qtbot, mocked_client):
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
c0 = wf.plot(x=[1, 2, 3], y=[1, 4, 9], name="Curve_0")
|
||||
c1 = wf.plot(x=[1, 2, 3], y=[2, 5, 10], name="Curve_1")
|
||||
wf.hook_crosshair()
|
||||
|
||||
# # Simulate a mouse move at (2,5)
|
||||
pos_in_view = QPointF(2, 5)
|
||||
pos_in_scene = wf.plot_item.vb.mapViewToScene(pos_in_view)
|
||||
event_mock = [pos_in_scene]
|
||||
|
||||
# 1) Both curves visible: expect markers for both
|
||||
wf.crosshair.clear_markers()
|
||||
wf.crosshair.mouse_moved(event_mock)
|
||||
assert set(wf.crosshair.marker_moved_1d.keys()) == {"Curve_0", "Curve_1"}
|
||||
|
||||
# 2) Hide Curve B and repeat: only Curve_0 should remain
|
||||
c1.setVisible(False)
|
||||
wf.crosshair.clear_markers()
|
||||
wf.crosshair.mouse_moved(event_mock)
|
||||
qtbot.wait(200)
|
||||
assert set(wf.crosshair.marker_moved_1d.keys()) == {"Curve_0"}
|
||||
|
||||
@@ -2,10 +2,15 @@ import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from bec_lib.scan_history import ScanHistory
|
||||
from qtpy.QtGui import QValidator
|
||||
from qtpy.QtWidgets import QComboBox, QVBoxLayout
|
||||
|
||||
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_setting import CurveSetting
|
||||
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_tree import CurveTree
|
||||
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_tree import (
|
||||
CurveTree,
|
||||
ScanIndexValidator,
|
||||
)
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
from tests.unit_tests.client_mocks import dap_plugin_message, mocked_client, mocked_client_with_dap
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
@@ -155,7 +160,7 @@ def test_curve_tree_init(curve_tree_fixture):
|
||||
curve_tree, wf = curve_tree_fixture
|
||||
assert curve_tree.waveform == wf
|
||||
assert curve_tree.color_palette == "plasma"
|
||||
assert curve_tree.tree.columnCount() == 7
|
||||
assert curve_tree.tree.columnCount() == 8
|
||||
|
||||
assert curve_tree.toolbar.components.exists("add")
|
||||
assert curve_tree.toolbar.components.exists("expand")
|
||||
@@ -374,3 +379,54 @@ def test_export_data_dap(curve_tree_fixture):
|
||||
assert exported["signal"]["entry"] == "bpm4i"
|
||||
assert exported["signal"]["dap"] == "GaussianModel"
|
||||
assert exported["label"] == "bpm4i-bpm4i-GaussianModel"
|
||||
|
||||
|
||||
def test_scan_index_validator_behavior():
|
||||
"""
|
||||
Test ScanIndexValidator allows empty, 'live', partial 'live', valid scan numbers,
|
||||
and rejects invalid or disallowed inputs under the new allowed-set API.
|
||||
"""
|
||||
validator = ScanIndexValidator(allowed_scans={1, 2, 3})
|
||||
|
||||
def state(txt):
|
||||
s, _, _ = validator.validate(txt, 0)
|
||||
return s
|
||||
|
||||
assert state("") == QValidator.State.Acceptable
|
||||
assert state("live") == QValidator.State.Acceptable
|
||||
assert state("l") == QValidator.State.Intermediate
|
||||
assert state("liv") == QValidator.State.Intermediate
|
||||
assert state("1") == QValidator.State.Acceptable
|
||||
assert state("3") == QValidator.State.Acceptable
|
||||
assert state("4") == QValidator.State.Invalid
|
||||
assert state("0") == QValidator.State.Invalid
|
||||
assert state("abc") == QValidator.State.Invalid
|
||||
|
||||
|
||||
def test_export_data_history_curve(curve_tree_fixture, scan_history_factory):
|
||||
"""
|
||||
Test that export_data for a history curve row correctly serializes scan_number
|
||||
and resets scan_id when a numeric scan is selected.
|
||||
"""
|
||||
curve_tree, wf = curve_tree_fixture
|
||||
# Inject two history scans into the waveform client
|
||||
msgs = [
|
||||
scan_history_factory(scan_id="hid1", scan_number=1),
|
||||
scan_history_factory(scan_id="hid2", scan_number=2),
|
||||
]
|
||||
wf.client.history = ScanHistory(wf.client, False)
|
||||
for m in msgs:
|
||||
wf.client.history._scan_data[m.scan_id] = m
|
||||
wf.client.history._scan_ids.append(m.scan_id)
|
||||
wf.client.history._scan_numbers.append(m.scan_number)
|
||||
wf.client.queue.scan_storage.current_scan = None
|
||||
|
||||
# Create a device row and select scan index "2"
|
||||
device_row = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
|
||||
device_row.scan_index_combo.setCurrentText("2")
|
||||
|
||||
exported = device_row.export_data()
|
||||
assert exported["source"] == "history"
|
||||
assert exported["scan_number"] == 2
|
||||
assert exported["scan_id"] is None
|
||||
assert exported["label"] == "bpm4i-bpm4i-scan-2"
|
||||
|
||||
@@ -9,7 +9,6 @@ from bec_widgets.utils.forms_from_types.items import FormItemSpec, ListFormItem
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 11), reason="Generic types don't support this in 3.10")
|
||||
@pytest.mark.parametrize(
|
||||
["input", "validity"],
|
||||
[
|
||||
|
||||
@@ -50,7 +50,7 @@ def positioner_box(qtbot, mocked_client):
|
||||
def test_positioner_box(positioner_box):
|
||||
"""Test init of positioner box"""
|
||||
assert positioner_box.device == "samx"
|
||||
data = positioner_box.dev["samx"].read()
|
||||
data = positioner_box.dev["samx"].read(cached=True)
|
||||
# Avoid check for Positioner class from BEC in _init_device
|
||||
|
||||
setpoint_text = positioner_box.ui.setpoint.text()
|
||||
|
||||
@@ -29,8 +29,8 @@ def test_positioner_box_2d(positioner_box_2d):
|
||||
"""Test init of 2D positioner box"""
|
||||
assert positioner_box_2d.device_hor == "samx"
|
||||
assert positioner_box_2d.device_ver == "samy"
|
||||
data_hor = positioner_box_2d.dev["samx"].read()
|
||||
data_ver = positioner_box_2d.dev["samy"].read()
|
||||
data_hor = positioner_box_2d.dev["samx"].read(cached=True)
|
||||
data_ver = positioner_box_2d.dev["samy"].read(cached=True)
|
||||
# Avoid check for Positioner class from BEC in _init_device
|
||||
|
||||
setpoint_hor_text = positioner_box_2d.ui.setpoint_hor.text()
|
||||
|
||||
@@ -10,22 +10,19 @@ import pyqtgraph as pg
|
||||
import pytest
|
||||
from pyqtgraph.graphicsItems.DateAxisItem import DateAxisItem
|
||||
from qtpy.QtCore import QTimer
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QDoubleSpinBox,
|
||||
QSpinBox,
|
||||
)
|
||||
from qtpy.QtWidgets import QApplication, QCheckBox, QDialog, QDialogButtonBox, QDoubleSpinBox
|
||||
|
||||
from bec_widgets.widgets.plots.plot_base import UIMode
|
||||
from bec_widgets.widgets.plots.waveform.curve import DeviceSignal
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
from bec_widgets.widgets.services.scan_history_browser.scan_history_browser import (
|
||||
ScanHistoryBrowser,
|
||||
)
|
||||
from tests.unit_tests.client_mocks import (
|
||||
DummyData,
|
||||
create_dummy_scan_item,
|
||||
dap_plugin_message,
|
||||
inject_scan_history,
|
||||
mocked_client,
|
||||
mocked_client_with_dap,
|
||||
)
|
||||
@@ -841,6 +838,33 @@ def test_show_dap_summary_popup(qtbot, mocked_client):
|
||||
assert fit_action.isChecked() is False
|
||||
|
||||
|
||||
def test_show_scan_history_popup(qtbot, mocked_client):
|
||||
"""
|
||||
Test that show_scan_history_popup displays the scan history browser dialog
|
||||
and toggles the toolbar action correctly.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
scan_action = wf.toolbar.components.get_action("scan_history").action
|
||||
# Initially unchecked and no dialog
|
||||
assert not scan_action.isChecked()
|
||||
assert wf.scan_history_dialog is None
|
||||
|
||||
# Show the popup
|
||||
wf.show_scan_history_popup()
|
||||
# Dialog should exist and be visible, action checked
|
||||
assert wf.scan_history_dialog is not None
|
||||
assert wf.scan_history_dialog.isVisible()
|
||||
assert scan_action.isChecked()
|
||||
# The embedded widget should be the correct type
|
||||
assert isinstance(wf.scan_history_widget, ScanHistoryBrowser)
|
||||
|
||||
# Close the dialog (triggers _scan_history_closed)
|
||||
wf.scan_history_dialog.close()
|
||||
# Dialog reference should be cleared and action unchecked
|
||||
assert wf.scan_history_dialog is None
|
||||
assert not scan_action.isChecked()
|
||||
|
||||
|
||||
#####################################################
|
||||
# The following tests are for the async dataset guard
|
||||
#####################################################
|
||||
@@ -1063,3 +1087,187 @@ def test_dialog_reject_real_interaction(qtbot, mocked_client):
|
||||
assert wf.skip_large_dataset_warning is True
|
||||
# Limit remains unchanged
|
||||
assert wf.max_dataset_size_mb == 1
|
||||
|
||||
|
||||
def test_update_with_scan_history_by_index(qtbot, mocked_client, scan_history_factory):
|
||||
"""
|
||||
Test that update_with_scan_history by index loads the correct historical scan.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
hist1, hist2 = inject_scan_history(wf, scan_history_factory, ("hist1", 1), ("hist2", 2))
|
||||
|
||||
assert len(wf.client.history._scan_ids) == 2, "Expected two history scans"
|
||||
|
||||
# Do history curve plotting
|
||||
wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="hist1")
|
||||
wf.plot(y_name="bpm4i", scan_number=2)
|
||||
|
||||
assert len(wf.plot_item.curves) == 2, "Expected two curves for history scans"
|
||||
c1, c2 = wf.plot_item.curves
|
||||
# First curve should be for hist1, second for hist2
|
||||
assert c1.config.signal.name == "bpm4i"
|
||||
assert c1.config.signal.entry == "bpm4i"
|
||||
assert c1.config.scan_id == "hist1"
|
||||
assert c1.config.scan_number == 1
|
||||
assert c1.name() == "bpm4i-bpm4i-scan-1"
|
||||
|
||||
assert c2.config.signal.name == "bpm4i"
|
||||
assert c2.config.signal.entry == "bpm4i"
|
||||
assert c2.config.scan_id == "hist2"
|
||||
assert c2.config.scan_number == 2
|
||||
assert c2.name() == "bpm4i-bpm4i-scan-2"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["auto", "timestamp", "index", "samx"])
|
||||
def test_history_curve_x_modes_pre_plot(qtbot, mocked_client, scan_history_factory, mode):
|
||||
"""
|
||||
Test that history curves respect x_mode when set before plotting.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
hist1, hist2 = inject_scan_history(wf, scan_history_factory, ("hist1", 1), ("hist2", 2))
|
||||
wf.x_mode = mode
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="hist1")
|
||||
assert c.config.current_x_mode == mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["auto", "timestamp", "index", "samx"])
|
||||
def test_history_curve_x_modes_post_plot(qtbot, mocked_client, scan_history_factory, mode):
|
||||
"""
|
||||
Test that changing x_mode after plotting history curves updates the curve on refresh.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
hist1, hist2 = inject_scan_history(wf, scan_history_factory, ("hist1", 1), ("hist2", 2))
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="hist1")
|
||||
# Change x_mode after plotting
|
||||
wf.x_mode = mode
|
||||
# Refresh history curves
|
||||
wf._refresh_history_curves()
|
||||
assert c.config.current_x_mode == mode
|
||||
|
||||
|
||||
def test_history_curve_incompatible_x_mode_hides_curve(qtbot, mocked_client, scan_history_factory):
|
||||
"""
|
||||
Test that setting an x_mode not present in stored data hides the history curve.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf.x_mode = "nonexistent_device"
|
||||
# Inject history scan for this test
|
||||
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_bad", 1))
|
||||
# Plot history curve
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
|
||||
# Curve should be hidden due to incompatible x_mode
|
||||
assert not c.isVisible()
|
||||
|
||||
|
||||
def test_fetch_history_data_no_stored_data_raises(
|
||||
qtbot, mocked_client, monkeypatch, suppress_message_box
|
||||
):
|
||||
"""
|
||||
Test that fetching history data when stored_data_info is missing raises ValueError.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
# Create a dummy scan_item lacking stored_data_info
|
||||
dummy_scan = SimpleNamespace(
|
||||
_msg=SimpleNamespace(stored_data_info=None),
|
||||
devices={},
|
||||
metadata={"bec": {"scan_id": "dummy", "scan_number": 1, "scan_report_devices": []}},
|
||||
)
|
||||
# Force get_history_scan_item to return our dummy
|
||||
monkeypatch.setattr(wf, "get_history_scan_item", lambda scan_id, scan_index: dummy_scan)
|
||||
# Attempt to plot history curve should be suppressed by SafeSlot and return None
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="dummy", scan_number=1)
|
||||
assert c is None
|
||||
assert len(wf.curves) == 0
|
||||
|
||||
|
||||
def test_history_curve_device_missing_returns_none(qtbot, mocked_client, scan_history_factory):
|
||||
"""
|
||||
If the y-device is not in stored_data_info, plot should return None.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf.x_mode = "index"
|
||||
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_dev_missing", 1))
|
||||
c = wf.plot(y_name="non-existing", y_entry="non-existing", scan_id=history_msg.scan_id)
|
||||
assert c is None
|
||||
|
||||
|
||||
def test_history_curve_custom_shape_mismatch_hides_curve(
|
||||
qtbot, mocked_client, scan_history_factory
|
||||
):
|
||||
"""
|
||||
For custom x-mode, if x and y shapes mismatch, curve should be hidden.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf.x_mode = "async_device"
|
||||
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_custom_shape", 1))
|
||||
# Force shape mismatch for x-data
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
|
||||
assert c is not None
|
||||
assert not c.isVisible()
|
||||
|
||||
|
||||
def test_history_curve_index_mode_plots_curve(qtbot, mocked_client, scan_history_factory):
|
||||
"""
|
||||
Test that setting x_mode to 'index' plots and shows the history curve correctly.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf.x_mode = "index"
|
||||
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_index", 1))
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
|
||||
assert c is not None
|
||||
assert c.isVisible()
|
||||
assert c.config.current_x_mode == "index"
|
||||
|
||||
|
||||
def test_history_curve_timestamp_mode_plots_curve(qtbot, mocked_client, scan_history_factory):
|
||||
"""
|
||||
Test that setting x_mode to 'timestamp' plots and shows the history curve correctly.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf.x_mode = "timestamp"
|
||||
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_time", 1))
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
|
||||
assert c is not None
|
||||
assert c.isVisible()
|
||||
assert c.config.current_x_mode == "timestamp"
|
||||
|
||||
|
||||
def test_history_curve_auto_valid_uses_first_report_device(
|
||||
qtbot, mocked_client, scan_history_factory
|
||||
):
|
||||
"""
|
||||
Test that 'auto' x_mode uses the first available report device and shows the curve.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf.x_mode = "auto"
|
||||
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_auto_valid", 1))
|
||||
# Plot history curve
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
|
||||
assert c is not None
|
||||
assert c.isVisible()
|
||||
# Should have fallen back to the first scan_report_device
|
||||
assert c.config.current_x_mode == "auto"
|
||||
|
||||
|
||||
def test_history_curve_file_not_found_returns_none(qtbot, mocked_client, scan_history_factory):
|
||||
"""
|
||||
If the history file path does not exist, plot should return None.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf.x_mode = "index"
|
||||
# Inject a valid history message then corrupt its file_path
|
||||
[history_msg] = inject_scan_history(wf, scan_history_factory, ("bad_file", 1))
|
||||
history_msg.file_path = "/nonexistent/path.h5"
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
|
||||
assert c is None
|
||||
|
||||
|
||||
def test_history_curve_scan_not_found_returns_none(qtbot, mocked_client):
|
||||
"""
|
||||
If the requested scan_id is not in history, plot should return None.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf.x_mode = "index"
|
||||
# No history scans injected for this widget
|
||||
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="unknown_scan")
|
||||
assert c is None
|
||||
Reference in New Issue
Block a user