mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-09 16:22:08 +02:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f12339e6f9 | |||
| ce8e5f0bec | |||
| 7ea9ab5175 | |||
| b72f0dc6e8 | |||
| cb9d429884 | |||
| 0a80bd0a92 | |||
| 9bc9d355e2 | |||
| 7d5e702a11 | |||
| 40cbf7fe4f | |||
| 7b287c45f2 | |||
| c9455672b5 | |||
| 7f06375f9d | |||
| d00d786399 | |||
| a4c465dcaf | |||
| d0e94d0da4 | |||
| bb3cea7fe8 | |||
| 3c6aa8e138 | |||
| 198684c65d | |||
| 617f2df2af | |||
| ef83287126 | |||
| d5e6f095fe | |||
| b10efc0f40 | |||
| 44b1dbf911 | |||
| e9d381a18a | |||
| b005542df3 | |||
| 13a9175ba5 | |||
| 3f8e60a14f | |||
| 6bc1c3c5f1 | |||
| 9f91eb2e08 | |||
| 1e19092319 | |||
| 96664c3923 | |||
| 741ca2fd8a | |||
| 3941050883 | |||
| 1d746c6829 | |||
| ef27de40ce | |||
| 37df95ead8 | |||
| c87a6cfce9 | |||
| 3d807eaa63 | |||
| 28ac9c5cc3 | |||
| 1dd20d5986 | |||
| 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 | |||
| ba047fd776 | |||
| 6e05157abb | |||
| f4bc759e72 | |||
| 1bec9bd9b2 | |||
| 8b013d5dce | |||
| f2e5a85e61 | |||
| a2f8880459 | |||
| 926d722955 | |||
| 44ba7201b4 | |||
| 0717426db2 | |||
| f4af6ebc5f | |||
| a923f12c97 | |||
| a5a7607a83 | |||
| 9de548446b | |||
| 49ac7decf7 | |||
| 092bed38fa | |||
| 50c84a766a | |||
| d22a3317ba | |||
| 6df1d0c31f | |||
| 946752a4b0 | |||
| c1f62ad6cb | |||
| a5adf3a97d | |||
| 76e3e0b60f | |||
| f18eeb9c5d | |||
| 32ce8e2818 | |||
| 23413cffab | |||
| 4bbb8fa519 | |||
| a972369a72 | |||
| cd81e7f9ba | |||
| e2b8118f67 | |||
| 5f925ba4e3 | |||
| fc68d2cf2d | |||
| 627b49b33a | |||
| a51ef04cdf | |||
| 40f4bce285 | |||
| 2b9fe6c959 | |||
| c2e16429c9 | |||
| 85ce2aa136 | |||
| fd5af01842 | |||
| 8a214c8978 | |||
| f3214445f2 | |||
| 6bf84aea25 | |||
| aace071f11 | |||
| bf86a030a0 | |||
| 358c979bf2 | |||
| c1bdc506e8 | |||
| 4febfb79df | |||
| 0854175acb | |||
| e090ac49b7 | |||
| e4521d9528 | |||
| 1d0490fff4 | |||
| 10cbb9a05c | |||
| 7073e75adf | |||
| e42ffd7c01 | |||
| 2bd6d00899 | |||
| c2a918ef4b | |||
| 6bbf5126cf | |||
| 728d4efd96 | |||
| 7926969996 | |||
| 61e5bde15f | |||
| c8aa770de3 | |||
| 4d5df9608a | |||
| b718b438ba |
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
python-version: ["3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BEC_WIDGETS_BRANCH: main # Set the branch you want for bec_widgets
|
BEC_WIDGETS_BRANCH: main # Set the branch you want for bec_widgets
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ name: 'Close stale issues and PRs'
|
|||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '00 10 * * *'
|
- cron: '00 10 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
stale-issue-message: 'This issue is stale because it has been open 120 days with no activity. Remove stale label or comment or this will be closed in 14 days.'
|
||||||
stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
stale-pr-message: 'This PR is stale because it has been open 120 days with no activity. Remove stale label or comment or this will be closed in 14 days.'
|
||||||
days-before-stale: 60
|
days-before-stale: 120
|
||||||
days-before-close: 7
|
days-before-close: 14
|
||||||
|
|||||||
-289
@@ -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
|
# Minimum Python version to use for version dependent checks. Will default to
|
||||||
# the version used to run pylint.
|
# 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
|
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||||
# user-friendly hints instead of false-positive error messages.
|
# user-friendly hints instead of false-positive error messages.
|
||||||
|
|||||||
+574
@@ -1,6 +1,580 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
|
||||||
|
## v2.45.7 (2025-12-08)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Handle none in literal combobox
|
||||||
|
([`ce8e5f0`](https://github.com/bec-project/bec_widgets/commit/ce8e5f0bec7643c9f826e06f987775de95abb91d))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.45.6 (2025-11-27)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **curve**: Update dap curves if data are set manually
|
||||||
|
([`b72f0dc`](https://github.com/bec-project/bec_widgets/commit/b72f0dc6e8474a65c83f7e2c938fc6356b7b5f3a))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.45.5 (2025-11-26)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Remove ghost widgets in scan metadata
|
||||||
|
([`0a80bd0`](https://github.com/bec-project/bec_widgets/commit/0a80bd0a9279cef1136a04c252c97e624ef2e779))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.45.4 (2025-11-24)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **main_window**: Removed hiding scan progressbar animation
|
||||||
|
([`40cbf7f`](https://github.com/bec-project/bec_widgets/commit/40cbf7fe4f834a1a65306e54b3882d2c0495f90a))
|
||||||
|
|
||||||
|
- **web_links**: Fixed link to bec widget issues from gitlab to github
|
||||||
|
([`7d5e702`](https://github.com/bec-project/bec_widgets/commit/7d5e702a11043ed96a8cb97fce6b2162681e8fab))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.45.3 (2025-11-17)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **fakeredis**: Add support for additional args
|
||||||
|
([`c945567`](https://github.com/bec-project/bec_widgets/commit/c9455672b58b9df101ccd0d80a169bdf6c707f34))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.45.2 (2025-11-17)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **test**: Removed duplicate test in crosshair
|
||||||
|
([`d00d786`](https://github.com/bec-project/bec_widgets/commit/d00d786399bca516b8030b9de881b674140bf439))
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
- Pyqtgraph pin to 0.13.7
|
||||||
|
([`a4c465d`](https://github.com/bec-project/bec_widgets/commit/a4c465dcaf8cb03962dec1e360b7b832a9a5c780))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.45.1 (2025-11-14)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **waveform**: Async_readback can accept 0D data
|
||||||
|
([`bb3cea7`](https://github.com/bec-project/bec_widgets/commit/bb3cea7fe800cd5375de5351a72e0944dc86861f))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.45.0 (2025-11-10)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
|
||||||
|
- Add third-party license notice
|
||||||
|
([`617f2df`](https://github.com/bec-project/bec_widgets/commit/617f2df2af41db7692c42d0e10bce4968f36fb94))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **waveform**: Dap curve can be attached to custom and history curves
|
||||||
|
([`198684c`](https://github.com/bec-project/bec_widgets/commit/198684c65d9565e8985156b426b8ef98dcc687cc))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.44.0 (2025-11-05)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
|
||||||
|
- Update stale issue and PR settings to 120 days
|
||||||
|
([`e9d381a`](https://github.com/bec-project/bec_widgets/commit/e9d381a18a425727216f035ecccdad25f3189608))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Readme rewritten
|
||||||
|
([`44b1dbf`](https://github.com/bec-project/bec_widgets/commit/44b1dbf911f43dbde4286e2ea541c480f7b834be))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **plot_base**: Invert x/y axis
|
||||||
|
([`b10efc0`](https://github.com/bec-project/bec_widgets/commit/b10efc0f400fe36f7cb0d5998214d50943934d7b))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- **plot_base**: Consolidated user access for the PlotBase
|
||||||
|
([`d5e6f09`](https://github.com/bec-project/bec_widgets/commit/d5e6f095fe60223972235acd3ea68389aa7a1a14))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.43.0 (2025-10-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add pdf viewer widget
|
||||||
|
([`13a9175`](https://github.com/bec-project/bec_widgets/commit/13a9175ba5f5e1e2404d7302404d9511872aafc7))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.42.1 (2025-10-28)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **rpc_server**: Raise window, even if minimized
|
||||||
|
([`6bc1c3c`](https://github.com/bec-project/bec_widgets/commit/6bc1c3c5f1b3e57ab8e8aeabcc1c0a52a56bbf0a))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.42.0 (2025-10-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **image_roi**: Enhance get_coordinates to include rectangle center and dimensions
|
||||||
|
([`96664c3`](https://github.com/bec-project/bec_widgets/commit/96664c3923737df0b09aa8f35df388f9fd630b55))
|
||||||
|
|
||||||
|
- **positioner_box_2d**: Added properties to enable/disable vertical and horizontal controls
|
||||||
|
([`1e19092`](https://github.com/bec-project/bec_widgets/commit/1e190923196f8b28c92dfdd83b9ce90873dd792d))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.41.1 (2025-10-15)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **dependencies**: Bec lib versions fixed
|
||||||
|
([`3941050`](https://github.com/bec-project/bec_widgets/commit/3941050883a791f800ab7178af2435ac14f837b6))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.41.0 (2025-10-15)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **image_roi**: Delete button added to compact version
|
||||||
|
([`ef27de4`](https://github.com/bec-project/bec_widgets/commit/ef27de40ceee8375d95a0f3a8e451b7d05d0ae2c))
|
||||||
|
|
||||||
|
- **image_roi**: Rois can be removed with right click context menu
|
||||||
|
([`37df95e`](https://github.com/bec-project/bec_widgets/commit/37df95ead8d6a07a6c5794a97a486d9f380004cc))
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
- **bec_lib**: Version bump to 3.69.3
|
||||||
|
([`28ac9c5`](https://github.com/bec-project/bec_widgets/commit/28ac9c5cc369bdfa712c70c45591243631c65066))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **image_roi_tree**: Compact mode added
|
||||||
|
([`c87a6cf`](https://github.com/bec-project/bec_widgets/commit/c87a6cfce9c36588b32f5279e63072bc2646c36f))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- **serializer**: Upgrade to new serializer interface
|
||||||
|
([`3d807ea`](https://github.com/bec-project/bec_widgets/commit/3d807eaa63980fd2bb11661696c4d8548fffde8c))
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- **deviceconfig-form-update**: Add onFailure default to test
|
||||||
|
([`1dd20d5`](https://github.com/bec-project/bec_widgets/commit/1dd20d5986485f3bfe7ee02596ca23027ec4b756))
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- **device_manager**: Devicemanager view of config session
|
||||||
|
([`6e05157`](https://github.com/bec-project/bec_widgets/commit/6e05157abb61ec569eec10ff7295c28cb6a2ec45))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.37.0 (2025-08-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add explorer widget
|
||||||
|
([`1bec9bd`](https://github.com/bec-project/bec_widgets/commit/1bec9bd9b2238ed484e8d25e691326efe5730f6b))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.36.0 (2025-08-18)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **scan control**: Add support for literals
|
||||||
|
([`f2e5a85`](https://github.com/bec-project/bec_widgets/commit/f2e5a85e616aa76d4b7ad3b3c76a24ba114ebdd1))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.35.0 (2025-08-14)
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
- Pyside6 upgraded to 6.9.0
|
||||||
|
([`44ba720`](https://github.com/bec-project/bec_widgets/commit/44ba7201b4914d63281bbed5e62d07e5c240595a))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **property_manager**: Property manager widget
|
||||||
|
([`926d722`](https://github.com/bec-project/bec_widgets/commit/926d7229559d189d382fe034b3afbc544e709efa))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.34.0 (2025-08-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Plugin widget import machinery
|
||||||
|
([`9de5484`](https://github.com/bec-project/bec_widgets/commit/9de548446b9975c0f692757c66ffa07b9a849f15))
|
||||||
|
|
||||||
|
- lazy import client so plugin widgets can import BECWidgets which use it indirectly - exclude
|
||||||
|
classes originating from bec_widgets core from plugin discovery - better errors
|
||||||
|
|
||||||
|
- Use better source for plugin repo name
|
||||||
|
([`f4af6eb`](https://github.com/bec-project/bec_widgets/commit/f4af6ebc5fabf5b62ec87b580476d93d52690b08))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Autoformat compiled file and add docs
|
||||||
|
([`a923f12`](https://github.com/bec-project/bec_widgets/commit/a923f12c974192909222fcada9eca97325866d74))
|
||||||
|
|
||||||
|
- **plugin manager**: Add cli commands
|
||||||
|
([`49ac7de`](https://github.com/bec-project/bec_widgets/commit/49ac7decf7d4cf461e6437f7285dc6967ee36d96))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.33.3 (2025-07-31)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **scan-history-view**: Account for async loading of scan history
|
||||||
|
([`6df1d0c`](https://github.com/bec-project/bec_widgets/commit/6df1d0c31fb58c25b01e95e2247277ff2dd5d00e))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- Improve scan history performance on loading full scan lists
|
||||||
|
([`a5adf3a`](https://github.com/bec-project/bec_widgets/commit/a5adf3a97d9ff05cef833445c1e6cd8f35a9a2fa))
|
||||||
|
|
||||||
|
- Make ids a set, cleanup
|
||||||
|
([`c1f62ad`](https://github.com/bec-project/bec_widgets/commit/c1f62ad6cb00d9b392a8e0b6247f5260dfb37256))
|
||||||
|
|
||||||
|
- Use client callback for scan history reload
|
||||||
|
([`d22a331`](https://github.com/bec-project/bec_widgets/commit/d22a3317baeccfcc4e074dcef4e3912301d210c5))
|
||||||
|
|
||||||
|
- **scan-history**: Add spinner for loading time of history
|
||||||
|
([`50c84a7`](https://github.com/bec-project/bec_widgets/commit/50c84a766a2b021768fb2c0e8ee00b8e5f058ba7))
|
||||||
|
|
||||||
|
- **scan-history**: Fix insert logic; cleanup
|
||||||
|
([`946752a`](https://github.com/bec-project/bec_widgets/commit/946752a4b05804c2f59cb5c21e4c1d11709a7d44))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.33.2 (2025-07-31)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Delete choice dialog on close
|
||||||
|
([`23413cf`](https://github.com/bec-project/bec_widgets/commit/23413cffabe721e35bb5bb726ec34d74dc4ffe05))
|
||||||
|
|
||||||
|
- Display short lists in SignalDisplay
|
||||||
|
([`4bbb8fa`](https://github.com/bec-project/bec_widgets/commit/4bbb8fa519e8a90eebfcfa34e157493c9baa7880))
|
||||||
|
|
||||||
|
- Don't warn on empty DeviceEdit init
|
||||||
|
([`f18eeb9`](https://github.com/bec-project/bec_widgets/commit/f18eeb9c5dccbd9348b6ee6d1477a8b7925d40fc))
|
||||||
|
|
||||||
|
- Remove config, directly set device+signal
|
||||||
|
([`32ce8e2`](https://github.com/bec-project/bec_widgets/commit/32ce8e2818ceacda87e48399e3ed4df0cabb2335))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.33.1 (2025-07-31)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **cli**: Ensure guis are not started twice
|
||||||
|
([`cd81e7f`](https://github.com/bec-project/bec_widgets/commit/cd81e7f9ba40be23f6b930d250f743276720b277))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.33.0 (2025-07-29)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **monaco**: Forward text changed signal
|
||||||
|
([`a51ef04`](https://github.com/bec-project/bec_widgets/commit/a51ef04cdf0ac8abdb7008d78b13c75b86ce9e06))
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
- Update bec and qtmonaco min dependencies
|
||||||
|
([`5f925ba`](https://github.com/bec-project/bec_widgets/commit/5f925ba4e3840219e4473d6346ece6746076f718))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **monaco**: Add insert, delete and lsp header
|
||||||
|
([`fc68d2c`](https://github.com/bec-project/bec_widgets/commit/fc68d2cf2d6b161d8e3b9fc9daf6185d9197deba))
|
||||||
|
|
||||||
|
- **monaco**: Add vim mode
|
||||||
|
([`627b49b`](https://github.com/bec-project/bec_widgets/commit/627b49b33a30e45b2bfecb57f090eecfa31af09d))
|
||||||
|
|
||||||
|
- **web console**: Add set_readonly method
|
||||||
|
([`c2e1642`](https://github.com/bec-project/bec_widgets/commit/c2e16429c91de7cc0e672ba36224e9031c1c4234))
|
||||||
|
|
||||||
|
- **web console**: Add signal to indicate when the js backend is initialized
|
||||||
|
([`2b9fe6c`](https://github.com/bec-project/bec_widgets/commit/2b9fe6c9590c8d18b7542307273176e118828681))
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- **web console**: Add tests for the web console
|
||||||
|
([`40f4bce`](https://github.com/bec-project/bec_widgets/commit/40f4bce2854bcf333ce261229bd1703b80ced538))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.32.0 (2025-07-29)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **dock area**: Add screenshot toolbar action
|
||||||
|
([`fd5af01`](https://github.com/bec-project/bec_widgets/commit/fd5af0184279400ca6d8e5d2042f31be88d180f3))
|
||||||
|
|
||||||
|
- **rpc_timeout**: Add decorator to override the rpc timeout
|
||||||
|
([`8a214c8`](https://github.com/bec-project/bec_widgets/commit/8a214c897899d0d94d5f262591a001c127d1b155))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.31.3 (2025-07-29)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **waveform**: Fallback mechanism for auto mode to use index if scan_report_devices are not
|
||||||
|
available
|
||||||
|
([`6bf84ae`](https://github.com/bec-project/bec_widgets/commit/6bf84aea2508ff01fe201c045ec055684da88593))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.31.2 (2025-07-29)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **bec widgets**: Always call cleanup of child widgets on cleanup
|
||||||
|
([`bf86a03`](https://github.com/bec-project/bec_widgets/commit/bf86a030a08b325a08e031ff71d0716a2f2f122b))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.31.1 (2025-07-29)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **image_base**: Fix cleanup of uninitialized image layer
|
||||||
|
([`c1bdc50`](https://github.com/bec-project/bec_widgets/commit/c1bdc506e8099f178acdccbe0e1109deeeaaca38))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.31.0 (2025-07-29)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **bec_main_window**: Main window have unified status bar on macOS
|
||||||
|
([`1d0490f`](https://github.com/bec-project/bec_widgets/commit/1d0490fff428d51f2cdb7d35a954a7cd62cbb65c))
|
||||||
|
|
||||||
|
- **color_button_native**: Removed BECWidget inheritance
|
||||||
|
([`e42ffd7`](https://github.com/bec-project/bec_widgets/commit/e42ffd7c015a026d8e0967ac6b5866cbbea7bfed))
|
||||||
|
|
||||||
|
- **decimal_spinbox**: Removed BECWidget inheritance
|
||||||
|
([`2bd6d00`](https://github.com/bec-project/bec_widgets/commit/2bd6d0089955172134afb4d39939890026ed43f0))
|
||||||
|
|
||||||
|
- **launch_window**: Logic for custom main window apps adjusted
|
||||||
|
([`e090ac4`](https://github.com/bec-project/bec_widgets/commit/e090ac49b72fa15ebf1c09164ff3c6de577cb939))
|
||||||
|
|
||||||
|
- **plugin_utils**: Plugins can be created from QWidgets, no need for BECWidget base class for
|
||||||
|
plugin creation
|
||||||
|
([`c2a918e`](https://github.com/bec-project/bec_widgets/commit/c2a918ef4b77ccd7fa43d1bc0b907d55a17a6c95))
|
||||||
|
|
||||||
|
- **scan_progressbar**: Added kwargs to init
|
||||||
|
([`7073e75`](https://github.com/bec-project/bec_widgets/commit/7073e75adf0eeb81f4f8e27eb99fc1b7a395c751))
|
||||||
|
|
||||||
|
- **utils**: Plugin template createWidget do not initialise widgets by default
|
||||||
|
([`728d4ef`](https://github.com/bec-project/bec_widgets/commit/728d4efd9646ffcecd7d1a2f70988a7d7c799124))
|
||||||
|
|
||||||
|
- **widgets**: Added missing __init__ files
|
||||||
|
([`6bbf512`](https://github.com/bec-project/bec_widgets/commit/6bbf5126cf586063ed08d6cd489d6a9af28eac35))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **bec_main_window**: Plugin and rpc created
|
||||||
|
([`e4521d9`](https://github.com/bec-project/bec_widgets/commit/e4521d95286bbc598c3c05f357d247d950477b71))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- **widgets**: All plugins regenerated
|
||||||
|
([`10cbb9a`](https://github.com/bec-project/bec_widgets/commit/10cbb9a05cb96a791448caff4ffc4115b76146d7))
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- **launch_window**: Mainwindow raise test removed, features is supported now
|
||||||
|
([`0854175`](https://github.com/bec-project/bec_widgets/commit/0854175acbda1d4de71358aec028539552a26448))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.30.6 (2025-07-26)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **waveform**: Autorange is applied with 150ms delay after curve is added
|
||||||
|
([`61e5bde`](https://github.com/bec-project/bec_widgets/commit/61e5bde15f0e1ebe185ddbe81cd71ad581ae6009))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.30.5 (2025-07-25)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **positioner-box**: Test to fix handling of none integer values for precision
|
||||||
|
([`b718b43`](https://github.com/bec-project/bec_widgets/commit/b718b438bacff6eb6cd6015f1a67dcf75c05dce4))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- **positioner-box**: Cleanup, accept float precision
|
||||||
|
([`4d5df96`](https://github.com/bec-project/bec_widgets/commit/4d5df9608a9438b9f6d7508c323eb3772e53f37d))
|
||||||
|
|
||||||
|
|
||||||
## v2.30.4 (2025-07-25)
|
## v2.30.4 (2025-07-25)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,81 +1,200 @@
|
|||||||
# BEC Widgets
|

|
||||||
|
|
||||||
|
# BEC Widgets
|
||||||
|
|
||||||
[](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml)
|
[](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml)
|
||||||
[](https://pypi.org/project/bec-widgets/)
|
[](https://pypi.org/project/bec-widgets/)
|
||||||
[](./LICENSE)
|
[](./LICENSE)
|
||||||
[](https://github.com/psf/black)
|
[](https://github.com/psf/black)
|
||||||
[](https://www.python.org)
|
[](https://www.python.org)
|
||||||
[](https://doc.qt.io/qtforpython/)
|
[](https://doc.qt.io/qtforpython/)
|
||||||
[](https://conventionalcommits.org)
|
[](https://conventionalcommits.org)
|
||||||
[](https://codecov.io/gh/bec-project/bec_widgets)
|
[](https://codecov.io/gh/bec-project/bec_widgets)
|
||||||
|
|
||||||
|
A modular PySide6(Qt6) toolkit for [BEC (Beamline Experiment Control)](https://github.com/bec-project/bec). Create
|
||||||
|
high-performance, dockable GUIs to move devices, run scans, and stream live or disk data—powered by Redis and a modular
|
||||||
|
plugin system.
|
||||||
|
|
||||||
**⚠️ Important Notice:**
|
## Highlights
|
||||||
|
|
||||||
🚨 **PyQt6 is no longer supported** due to incompatibilities with Qt Designer. Please use **PySide6** instead. 🚨
|
- **No-code first** — For ~90% of day-to-day workflows, you can compose, operate, and save workspaces **without writing
|
||||||
|
a single line of code**. Just launch, drag widgets, and do your experiment.
|
||||||
|
- **Flexible layout composition** — Build complex experiment GUIs in seconds with the `BECDockArea`: drag‑dock, tab,
|
||||||
|
split, and export profiles/workspaces for reuse.
|
||||||
|
- **CLI / scripting** — Control your beamline experiment from the command line a robust RPC layer using
|
||||||
|
`BECIPythonClient`.
|
||||||
|
- **Designer integration** — Use Qt Designer plugins to drop BEC widgets next to any Qt control, then launch the `.ui`
|
||||||
|
with the custom BEC loader for a zero‑glue workflow.
|
||||||
|
- **Operational integration** — Widgets stay in sync with your running BEC/Redis as the single source of truth:
|
||||||
|
Subscribe to events from BEC and create dynamically updating UIs. BECWidgets also grants you easy access the
|
||||||
|
acquisition history.
|
||||||
|
- **Extensible by design** — Build new widgets with minimal boilerplate using `BECWidget` and `BECDispatcher` for BEC data and
|
||||||
|
messaging. Use the generator command to scaffold RPC interfaces and Designer plugin stubs; beamline plugins can extend
|
||||||
|
or override behavior as needed.
|
||||||
|
|
||||||
BEC Widgets is a GUI framework designed for interaction with [BEC (Beamline Experiment Control)](https://gitlab.psi.ch/bec/bec).
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Features](#features)
|
||||||
|
- [1. Dock area interface: build GUIs in seconds](#1-dock-area-interface-build-guis-in-seconds)
|
||||||
|
- [2. Qt Designer plugins + BEC Launcher (no glue)](#2-qt-designer-plugins--bec-launcher-no-glue)
|
||||||
|
- [3. Robust RPC from CLI & remote scripting](#3-robust-rpc-from-cli--remote-scripting)
|
||||||
|
- [4. Rapid development (extensible by design)](#4-rapid-development-extensible-by-design)
|
||||||
|
- [Widget Library](#widget-library)
|
||||||
|
- [Documentation](#documentation)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
Use any of the following setups:
|
||||||
|
|
||||||
|
### Stable release
|
||||||
|
|
||||||
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install BEC Widgets:
|
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install BEC Widgets:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install bec_widgets[pyside6]
|
pip install bec_widgets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### From source (recommended for development)
|
||||||
|
|
||||||
For development purposes, you can clone the repository and install the package locally in editable mode:
|
For development purposes, you can clone the repository and install the package locally in editable mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitlab.psi.ch/bec/bec-widgets
|
git clone https://github.com/bec-project/bec_widgets.git
|
||||||
cd bec_widgets
|
cd bec_widgets
|
||||||
pip install -e .[dev,pyside6]
|
pip install -e .[dev]
|
||||||
```
|
```
|
||||||
|
|
||||||
BEC Widgets now **only supports PySide6**. Users must manually install PySide6 as no default Qt distribution is
|
## Features
|
||||||
specified.
|
|
||||||
|
### 1. Dock area interface: build GUIs in seconds
|
||||||
|
|
||||||
|
The fastest way to explore BEC Widgets. Launch the BEC IPython client with simply `bec` in terminal and the **BECDockArea** opens as the default UI:
|
||||||
|
drag widgets, dock/tab/split panes, and explore. Everything is live—widgets auto-connect to BEC/Redis, so you can
|
||||||
|
operate immediately and refine later with RPC or Designer if needed.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 2. Qt Designer plugins + BEC Launcher (no glue)
|
||||||
|
|
||||||
|
All BEC Widgets ship as **Qt Designer plugins** with our custom Qt Designer launchable by `bec-designer`. Design your UI
|
||||||
|
visually in Designer, save a `.ui`, then launch it with
|
||||||
|
the **BEC Launcher**—no glue code. Widgets auto‑connect to BEC/Redis on startup, so your UI is operational immediately.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 3. Robust RPC from CLI & remote scripting
|
||||||
|
|
||||||
|
Operate and automate BEC Widgets directly from the `BECIPythonClient`. Create or attach to GUIs, address any sub-widget
|
||||||
|
via a simple hierarchical API with tab-completion, and script event-driven behavior that reacts to BEC (scan lifecycle,
|
||||||
|
active devices, topics)—so your UI can be heavily automated.
|
||||||
|
|
||||||
|
- Create & control GUIs: launch, load profiles, open/close panels, tweak properties—all from the shell.
|
||||||
|
- Hierarchical addressing: navigate widgets and sub-widgets with discoverable paths and tab-completion.
|
||||||
|
- Event scripting: subscribe to BEC events (e.g., scan start/finish, device readiness, topic updates) and trigger
|
||||||
|
actions,switch profiles, open diagnostic views, or start specific scans.
|
||||||
|
- Remote & headless: run automation on analysis nodes or from notebooks without a local GUI process.
|
||||||
|
- Plays with no-code: Use the Dock Area / BEC Designer to set up the layout and add automation with RPC when needed.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 4. Rapid development (extensible by design)
|
||||||
|
|
||||||
|
Build new widgets fast: Inherit from `BECWidget`, list your RPC methods in `USER_ACCESS`, and use `bec_dispatcher` to
|
||||||
|
bind endpoints. Then run `bw-generate-cli --target <your-plugin-repo>`. This generates the RPC CLI bindings and a Qt
|
||||||
|
Designer plugin that are immediately usable with your BEC setup. Widgets
|
||||||
|
come online with live BEC/Redis wiring out of the box. 
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> View code: Example Widget </summary>
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QWidget, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QApplication
|
||||||
|
from qtpy.QtCore import Slot
|
||||||
|
|
||||||
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
from bec_widgets import BECWidget, SafeSlot
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleMotorWidget(BECWidget, QWidget):
|
||||||
|
USER_ACCESS = ["move"]
|
||||||
|
|
||||||
|
def __init__(self, parent=None, motor_name="samx", step=5.0, **kwargs):
|
||||||
|
super().__init__(parent=parent, **kwargs)
|
||||||
|
self.motor_name = motor_name
|
||||||
|
self.step = float(step)
|
||||||
|
|
||||||
|
self.get_bec_shortcuts()
|
||||||
|
|
||||||
|
self.value_label = QLabel(f"{self.motor_name}: —")
|
||||||
|
self.btn_left = QPushButton("◀︎ -5")
|
||||||
|
self.btn_right = QPushButton("+5 ▶︎")
|
||||||
|
|
||||||
|
row = QHBoxLayout()
|
||||||
|
row.addWidget(self.btn_left)
|
||||||
|
row.addWidget(self.btn_right)
|
||||||
|
|
||||||
|
col = QVBoxLayout(self)
|
||||||
|
col.addWidget(self.value_label)
|
||||||
|
col.addLayout(row)
|
||||||
|
|
||||||
|
self.btn_left.clicked.connect(lambda: self.move("left", self.step))
|
||||||
|
self.btn_right.clicked.connect(lambda: self.move("right", self.step))
|
||||||
|
|
||||||
|
self.bec_dispatcher.connect_slot(self.on_readback, MessageEndpoints.device_readback(self.motor_name))
|
||||||
|
|
||||||
|
@SafeSlot(dict, dict)
|
||||||
|
def on_readback(self, data: dict, meta: dict):
|
||||||
|
current_value = data.get("signals").get(self.motor_name).get('value')
|
||||||
|
self.value_label.setText(f"{self.motor_name}: {current_value:.3f}")
|
||||||
|
|
||||||
|
@Slot(str, float)
|
||||||
|
def move(self, direction: Literal["left", "right"] = "left", step: float = 5.0):
|
||||||
|
if direction == "left":
|
||||||
|
self.dev[self.motor_name].move(-step, relative=True)
|
||||||
|
else:
|
||||||
|
self.dev[self.motor_name].move(step, relative=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
w = SimpleMotorWidget(motor_name="samx", step=5.0)
|
||||||
|
w.setWindowTitle("MotorJogWidget")
|
||||||
|
w.resize(280, 90)
|
||||||
|
w.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Widget Library
|
||||||
|
|
||||||
|
A large and growing catalog—plug, configure, run:
|
||||||
|
|
||||||
|
### Plotting
|
||||||
|
|
||||||
|
Waveform, MultiWaveform, and Image/Heatmap widgets deliver responsive plots with crosshairs and ROIs for live and
|
||||||
|
history data.
|
||||||
|
|
||||||
|
<img width="1108" height="838" alt="plotting_hr" src="https://github.com/user-attachments/assets/f50462a5-178d-44d4-aee5-d378c74b107b" />
|
||||||
|
|
||||||
|
### Scan orchestration and motion control.
|
||||||
|
|
||||||
|
Start and stop scans, track progress, reuse parameter presets, and browse history from a focused control surface.
|
||||||
|
Positioner boxes and tweak controls handle precise moves, homing, and calibration for day‑to‑day alignment.
|
||||||
|
|
||||||
|
<img width="1496" height="1388" alt="control" src="https://github.com/user-attachments/assets/d4fb2e2e-04f9-4621-8087-790680797620" />
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Documentation of BEC Widgets can be found [here](https://bec-widgets.readthedocs.io/en/latest/). The documentation of the BEC can be found [here](https://bec.readthedocs.io/en/latest/).
|
Documentation of BEC Widgets can be found [here](https://bec-widgets.readthedocs.io/en/latest/). The documentation of
|
||||||
|
the BEC can be found [here](https://bec.readthedocs.io/en/latest/).
|
||||||
## Contributing
|
|
||||||
|
|
||||||
All commits should use the Angular commit scheme:
|
|
||||||
|
|
||||||
> #### <a name="commit-header"></a>Angular Commit Message Header
|
|
||||||
>
|
|
||||||
> ```
|
|
||||||
> <type>(<scope>): <short summary>
|
|
||||||
> │ │ │
|
|
||||||
> │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
|
|
||||||
> │ │
|
|
||||||
> │ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
|
|
||||||
> │ elements|forms|http|language-service|localize|platform-browser|
|
|
||||||
> │ platform-browser-dynamic|platform-server|router|service-worker|
|
|
||||||
> │ upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve|
|
|
||||||
> │ devtools
|
|
||||||
> │
|
|
||||||
> └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
|
|
||||||
> ```
|
|
||||||
>
|
|
||||||
> The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
|
|
||||||
|
|
||||||
> ##### Type
|
|
||||||
>
|
|
||||||
> Must be one of the following:
|
|
||||||
>
|
|
||||||
> * **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
|
||||||
> * **ci**: Changes to our CI configuration files and scripts (examples: CircleCi, SauceLabs)
|
|
||||||
> * **docs**: Documentation only changes
|
|
||||||
> * **feat**: A new feature
|
|
||||||
> * **fix**: A bug fix
|
|
||||||
> * **perf**: A code change that improves performance
|
|
||||||
> * **refactor**: A code change that neither fixes a bug nor adds a feature
|
|
||||||
> * **test**: Adding missing tests or correcting existing tests
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/)
|
[BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
While BEC Widgets is shipped with BSD-3-Clause license, it includes third-party components with different licenses. Below is a list of these components along with their respective licenses.
|
||||||
|
|
||||||
|
Core Dependencies:
|
||||||
|
- BEC: BSD-3-Clause License, see [here](https://github.com/bec-project/bec/blob/main/LICENSE)
|
||||||
|
- black: MIT License, see [here](https://github.com/psf/black/blob/main/LICENSE)
|
||||||
|
- isort: MIT License, see [here](https://github.com/PyCQA/isort/blob/main/LICENSE)
|
||||||
|
- pydantic: MIT License, see [here](https://github.com/pydantic/pydantic/blob/main/LICENSE)
|
||||||
|
- pyqtgraph: MIT License, see [here](https://github.com/pyqtgraph/pyqtgraph/blob/master/LICENSE.txt)
|
||||||
|
- PySide6: LGPLv3 License, see [here](https://doc.qt.io/qtforpython/licenses.html)
|
||||||
|
- qtconsole: BSD-3-Clause License, see [here](https://github.com/spyder-ide/qtconsole/blob/main/LICENSE)
|
||||||
|
- qtpy: MIT License, see [here](https://github.com/spyder-ide/qtpy/blob/master/LICENSE.txt)
|
||||||
|
- qtmonaco: BSD-3-Clause License, see [here](https://github.com/bec-project/qtmonaco/blob/main/LICENSE)
|
||||||
|
- thefuzz: MIT License, see [here](https://github.com/seatgeek/thefuzz/blob/master/LICENSE.txt)
|
||||||
|
|
||||||
|
|
||||||
|
Additional Dependencies (Testing/Development):
|
||||||
|
- coverage: Apache License 2.0, see [here](https://github.com/coveragepy/coveragepy/blob/main/LICENSE.txt)
|
||||||
|
- fakeredis: BSD-3-Clause License, see [here](https://github.com/cunla/fakeredis-py/blob/master/LICENSE)
|
||||||
|
- pytest-bec-e2e: BSD-3-Clause License, see [here](https://github.com/bec-project/bec/blob/main/LICENSE)
|
||||||
|
- pytest-qt: MIT License, see [here](https://github.com/pytest-dev/pytest-qt/blob/master/LICENSE)
|
||||||
|
- pytest-random-order: MIT License, see [here](https://github.com/pytest-dev/pytest-random-order/blob/main/LICENSE)
|
||||||
|
- pytest-timeout: MIT License, see [here](https://github.com/pytest-dev/pytest-timeout/blob/main/LICENSE)
|
||||||
|
- pytest-xvfb: MIT License, see [here](https://github.com/The-Compiler/pytest-xvfb/blob/master/LICENSE)
|
||||||
|
- pytest: MIT License, see [here](https://github.com/pytest-dev/pytest/blob/main/LICENSE)
|
||||||
|
- pytest-cov: MIT License, see [here](https://github.com/pytest-dev/pytest-cov/blob/main/LICENSE)
|
||||||
|
- watchdog: Apache License 2.0, see [here](https://github.com/gorakhargosh/watchdog/blob/master/LICENSE)
|
||||||
|
- pre_commit: MIT License, see [here](https://github.com/pre-commit/pre-commit/blob/main/LICENSE)
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
|||||||
from bec_widgets.utils.ui_loader import UILoader
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
||||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, UILaunchWindow
|
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
|
||||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -395,20 +395,24 @@ class LaunchWindow(BECMainWindow):
|
|||||||
if isinstance(result_widget, BECMainWindow):
|
if isinstance(result_widget, BECMainWindow):
|
||||||
result_widget.show()
|
result_widget.show()
|
||||||
else:
|
else:
|
||||||
window = BECMainWindow()
|
window = BECMainWindowNoRPC()
|
||||||
window.setCentralWidget(result_widget)
|
window.setCentralWidget(result_widget)
|
||||||
window.setWindowTitle(f"BEC - {result_widget.objectName()}")
|
window.setWindowTitle(f"BEC - {result_widget.objectName()}")
|
||||||
window.show()
|
window.show()
|
||||||
return result_widget
|
return result_widget
|
||||||
|
|
||||||
def _launch_custom_ui_file(self, ui_file: str | None) -> BECMainWindow:
|
def _launch_custom_ui_file(self, ui_file: str | None) -> BECMainWindow:
|
||||||
# Load the custom UI file
|
"""
|
||||||
|
Load a custom .ui file. If the top-level widget is a MainWindow subclass,
|
||||||
|
instantiate it directly; otherwise, embed it in a UILaunchWindow.
|
||||||
|
"""
|
||||||
if ui_file is None:
|
if ui_file is None:
|
||||||
raise ValueError("UI file must be provided for custom UI file launch.")
|
raise ValueError("UI file must be provided for custom UI file launch.")
|
||||||
filename = os.path.basename(ui_file).split(".")[0]
|
filename = os.path.basename(ui_file).split(".")[0]
|
||||||
|
|
||||||
WidgetContainerUtils.raise_for_invalid_name(filename)
|
WidgetContainerUtils.raise_for_invalid_name(filename)
|
||||||
|
|
||||||
|
# Parse the UI to detect top-level widget class
|
||||||
tree = ET.parse(ui_file)
|
tree = ET.parse(ui_file)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
# Check if the top-level widget is a QMainWindow
|
# Check if the top-level widget is a QMainWindow
|
||||||
@@ -416,19 +420,22 @@ class LaunchWindow(BECMainWindow):
|
|||||||
if widget is None:
|
if widget is None:
|
||||||
raise ValueError("No widget found in the UI file.")
|
raise ValueError("No widget found in the UI file.")
|
||||||
|
|
||||||
if widget.attrib.get("class") == "QMainWindow":
|
# Load the UI into a widget
|
||||||
raise ValueError(
|
loader = UILoader(None)
|
||||||
"Loading a QMainWindow from a UI file is currently not supported. "
|
loaded = loader.loader(ui_file)
|
||||||
"If you need this, please contact the BEC team or create a ticket on gitlab.psi.ch/bec/bec_widgets."
|
|
||||||
)
|
# Display the UI in a BECMainWindow
|
||||||
|
if isinstance(loaded, BECMainWindow):
|
||||||
|
window = loaded
|
||||||
|
window.object_name = filename
|
||||||
|
else:
|
||||||
|
window = BECMainWindow(object_name=filename)
|
||||||
|
window.setCentralWidget(loaded)
|
||||||
|
|
||||||
window = UILaunchWindow(object_name=filename)
|
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
result_widget = UILoader(window).loader(ui_file)
|
window.setWindowTitle(f"BEC - {filename}")
|
||||||
window.setCentralWidget(result_widget)
|
|
||||||
window.setWindowTitle(f"BEC - {window.object_name}")
|
|
||||||
window.show()
|
window.show()
|
||||||
logger.info(f"Object name of new instance: {result_widget.objectName()}, {window.gui_id}")
|
logger.info(f"Launched custom UI: {filename}, type: {type(window).__name__}")
|
||||||
return window
|
return window
|
||||||
|
|
||||||
def _launch_auto_update(self, auto_update: str) -> AutoUpdates:
|
def _launch_auto_update(self, auto_update: str) -> AutoUpdates:
|
||||||
@@ -451,7 +458,7 @@ class LaunchWindow(BECMainWindow):
|
|||||||
|
|
||||||
WidgetContainerUtils.raise_for_invalid_name(name)
|
WidgetContainerUtils.raise_for_invalid_name(name)
|
||||||
|
|
||||||
window = BECMainWindow()
|
window = BECMainWindowNoRPC()
|
||||||
|
|
||||||
widget_instance = widget(root_widget=True, object_name=name)
|
widget_instance = widget(root_widget=True, object_name=name)
|
||||||
assert isinstance(widget_instance, QWidget)
|
assert isinstance(widget_instance, QWidget)
|
||||||
|
|||||||
+553
-65
@@ -12,7 +12,7 @@ from typing import Literal, Optional
|
|||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
|
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
|
||||||
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module
|
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
@@ -29,6 +29,7 @@ class _WidgetsEnumType(str, enum.Enum):
|
|||||||
_Widgets = {
|
_Widgets = {
|
||||||
"AbortButton": "AbortButton",
|
"AbortButton": "AbortButton",
|
||||||
"BECDockArea": "BECDockArea",
|
"BECDockArea": "BECDockArea",
|
||||||
|
"BECMainWindow": "BECMainWindow",
|
||||||
"BECProgressBar": "BECProgressBar",
|
"BECProgressBar": "BECProgressBar",
|
||||||
"BECQueue": "BECQueue",
|
"BECQueue": "BECQueue",
|
||||||
"BECStatusBox": "BECStatusBox",
|
"BECStatusBox": "BECStatusBox",
|
||||||
@@ -44,15 +45,18 @@ _Widgets = {
|
|||||||
"MonacoWidget": "MonacoWidget",
|
"MonacoWidget": "MonacoWidget",
|
||||||
"MotorMap": "MotorMap",
|
"MotorMap": "MotorMap",
|
||||||
"MultiWaveform": "MultiWaveform",
|
"MultiWaveform": "MultiWaveform",
|
||||||
|
"PdfViewerWidget": "PdfViewerWidget",
|
||||||
"PositionIndicator": "PositionIndicator",
|
"PositionIndicator": "PositionIndicator",
|
||||||
"PositionerBox": "PositionerBox",
|
"PositionerBox": "PositionerBox",
|
||||||
"PositionerBox2D": "PositionerBox2D",
|
"PositionerBox2D": "PositionerBox2D",
|
||||||
"PositionerControlLine": "PositionerControlLine",
|
"PositionerControlLine": "PositionerControlLine",
|
||||||
|
"PositionerGroup": "PositionerGroup",
|
||||||
"ResetButton": "ResetButton",
|
"ResetButton": "ResetButton",
|
||||||
"ResumeButton": "ResumeButton",
|
"ResumeButton": "ResumeButton",
|
||||||
"RingProgressBar": "RingProgressBar",
|
"RingProgressBar": "RingProgressBar",
|
||||||
"SBBMonitor": "SBBMonitor",
|
"SBBMonitor": "SBBMonitor",
|
||||||
"ScanControl": "ScanControl",
|
"ScanControl": "ScanControl",
|
||||||
|
"ScanProgressBar": "ScanProgressBar",
|
||||||
"ScatterWaveform": "ScatterWaveform",
|
"ScatterWaveform": "ScatterWaveform",
|
||||||
"SignalComboBox": "SignalComboBox",
|
"SignalComboBox": "SignalComboBox",
|
||||||
"SignalLabel": "SignalLabel",
|
"SignalLabel": "SignalLabel",
|
||||||
@@ -411,6 +415,13 @@ class BECDockArea(RPCBase):
|
|||||||
dict: The state of the dock area.
|
dict: The state of the dock area.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def restore_state(
|
def restore_state(
|
||||||
self, state: "dict" = None, missing: "Literal['ignore', 'error']" = "ignore", extra="bottom"
|
self, state: "dict" = None, missing: "Literal['ignore', 'error']" = "ignore", extra="bottom"
|
||||||
@@ -425,6 +436,14 @@ class BECDockArea(RPCBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BECMainWindow(RPCBase):
|
||||||
|
@rpc_call
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
Cleanup the BECConnector
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BECProgressBar(RPCBase):
|
class BECProgressBar(RPCBase):
|
||||||
"""A custom progress bar with smooth transitions. The displayed text can be customized using a template."""
|
"""A custom progress bar with smooth transitions. The displayed text can be customized using a template."""
|
||||||
|
|
||||||
@@ -1186,6 +1205,12 @@ class EllipticalROI(RPCBase):
|
|||||||
class Heatmap(RPCBase):
|
class Heatmap(RPCBase):
|
||||||
"""Heatmap widget for visualizing 2d grid data with color mapping for the z-axis."""
|
"""Heatmap widget for visualizing 2d grid data with color mapping for the z-axis."""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
Cleanup the BECConnector
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def enable_toolbar(self) -> "bool":
|
def enable_toolbar(self) -> "bool":
|
||||||
@@ -1373,6 +1398,29 @@ class Heatmap(RPCBase):
|
|||||||
Show the outer axes of the plot widget.
|
Show the outer axes of the plot widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def lock_aspect_ratio(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Whether the aspect ratio is locked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@lock_aspect_ratio.setter
|
||||||
|
@rpc_call
|
||||||
|
def lock_aspect_ratio(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Whether the aspect ratio is locked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def auto_range(self, value: "bool" = True):
|
||||||
|
"""
|
||||||
|
On demand apply autorange to the plot item based on the visible curves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(bool): If True, apply autorange to the visible curves.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def auto_range_x(self) -> "bool":
|
def auto_range_x(self) -> "bool":
|
||||||
@@ -1401,6 +1449,48 @@ class Heatmap(RPCBase):
|
|||||||
Set auto range for the y-axis.
|
Set auto range for the y-axis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def x_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set X-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@x_log.setter
|
||||||
|
@rpc_call
|
||||||
|
def x_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set X-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def y_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set Y-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@y_log.setter
|
||||||
|
@rpc_call
|
||||||
|
def y_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set Y-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def legend_label_size(self) -> "int":
|
||||||
|
"""
|
||||||
|
The font size of the legend font.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@legend_label_size.setter
|
||||||
|
@rpc_call
|
||||||
|
def legend_label_size(self) -> "int":
|
||||||
|
"""
|
||||||
|
The font size of the legend font.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def minimal_crosshair_precision(self) -> "int":
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
@@ -1415,6 +1505,13 @@ class Heatmap(RPCBase):
|
|||||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def color_map(self) -> "str":
|
def color_map(self) -> "str":
|
||||||
@@ -1471,20 +1568,6 @@ class Heatmap(RPCBase):
|
|||||||
Get the maximum value of the v_range.
|
Get the maximum value of the v_range.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
|
||||||
@rpc_call
|
|
||||||
def lock_aspect_ratio(self) -> "bool":
|
|
||||||
"""
|
|
||||||
Whether the aspect ratio is locked.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@lock_aspect_ratio.setter
|
|
||||||
@rpc_call
|
|
||||||
def lock_aspect_ratio(self) -> "bool":
|
|
||||||
"""
|
|
||||||
Whether the aspect ratio is locked.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def autorange(self) -> "bool":
|
def autorange(self) -> "bool":
|
||||||
@@ -1724,6 +1807,12 @@ class Heatmap(RPCBase):
|
|||||||
class Image(RPCBase):
|
class Image(RPCBase):
|
||||||
"""Image widget for displaying 2D data."""
|
"""Image widget for displaying 2D data."""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
Cleanup the BECConnector
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def enable_toolbar(self) -> "bool":
|
def enable_toolbar(self) -> "bool":
|
||||||
@@ -1911,6 +2000,29 @@ class Image(RPCBase):
|
|||||||
Show the outer axes of the plot widget.
|
Show the outer axes of the plot widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def lock_aspect_ratio(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Whether the aspect ratio is locked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@lock_aspect_ratio.setter
|
||||||
|
@rpc_call
|
||||||
|
def lock_aspect_ratio(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Whether the aspect ratio is locked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def auto_range(self, value: "bool" = True):
|
||||||
|
"""
|
||||||
|
On demand apply autorange to the plot item based on the visible curves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(bool): If True, apply autorange to the visible curves.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def auto_range_x(self) -> "bool":
|
def auto_range_x(self) -> "bool":
|
||||||
@@ -1939,6 +2051,48 @@ class Image(RPCBase):
|
|||||||
Set auto range for the y-axis.
|
Set auto range for the y-axis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def x_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set X-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@x_log.setter
|
||||||
|
@rpc_call
|
||||||
|
def x_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set X-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def y_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set Y-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@y_log.setter
|
||||||
|
@rpc_call
|
||||||
|
def y_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set Y-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def legend_label_size(self) -> "int":
|
||||||
|
"""
|
||||||
|
The font size of the legend font.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@legend_label_size.setter
|
||||||
|
@rpc_call
|
||||||
|
def legend_label_size(self) -> "int":
|
||||||
|
"""
|
||||||
|
The font size of the legend font.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def minimal_crosshair_precision(self) -> "int":
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
@@ -1953,6 +2107,13 @@ class Image(RPCBase):
|
|||||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def color_map(self) -> "str":
|
def color_map(self) -> "str":
|
||||||
@@ -2009,20 +2170,6 @@ class Image(RPCBase):
|
|||||||
Get the maximum value of the v_range.
|
Get the maximum value of the v_range.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
|
||||||
@rpc_call
|
|
||||||
def lock_aspect_ratio(self) -> "bool":
|
|
||||||
"""
|
|
||||||
Whether the aspect ratio is locked.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@lock_aspect_ratio.setter
|
|
||||||
@rpc_call
|
|
||||||
def lock_aspect_ratio(self) -> "bool":
|
|
||||||
"""
|
|
||||||
Whether the aspect ratio is locked.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def autorange(self) -> "bool":
|
def autorange(self) -> "bool":
|
||||||
@@ -2186,7 +2333,7 @@ class Image(RPCBase):
|
|||||||
Set the image source and update the image.
|
Set the image source and update the image.
|
||||||
|
|
||||||
Args:
|
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".
|
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_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".
|
color_bar(str): The type of color bar to use. Options are "simple" or "full".
|
||||||
@@ -2437,6 +2584,26 @@ class MonacoWidget(RPCBase):
|
|||||||
Get the current text from the Monaco editor.
|
Get the current text from the Monaco editor.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def insert_text(self, text: str, line: int | None = None, column: int | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Insert text at the current cursor position or at a specified line and column.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The text to insert.
|
||||||
|
line (int, optional): The line number (1-based) to insert the text at. Defaults to None.
|
||||||
|
column (int, optional): The column number (1-based) to insert the text at. Defaults to None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def delete_line(self, line: int | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Delete a line in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line (int, optional): The line number (1-based) to delete. If None, the current line will be deleted.
|
||||||
|
"""
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def set_language(self, language: str) -> None:
|
def set_language(self, language: str) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -2510,10 +2677,44 @@ class MonacoWidget(RPCBase):
|
|||||||
enabled (bool): If True, the minimap will be enabled; otherwise, it will be disabled.
|
enabled (bool): If True, the minimap will be enabled; otherwise, it will be disabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_vim_mode_enabled(self, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
Enable or disable Vim mode in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled (bool): If True, Vim mode will be enabled; otherwise, it will be disabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_lsp_header(self, header: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the LSP (Language Server Protocol) header for the Monaco editor.
|
||||||
|
The header is used to provide context for language servers but is not displayed in the editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
header (str): The LSP header to set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def get_lsp_header(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the current LSP header set in the Monaco editor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The LSP header.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class MotorMap(RPCBase):
|
class MotorMap(RPCBase):
|
||||||
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
|
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
Cleanup the BECConnector
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def enable_toolbar(self) -> "bool":
|
def enable_toolbar(self) -> "bool":
|
||||||
@@ -2715,6 +2916,15 @@ class MotorMap(RPCBase):
|
|||||||
Lock aspect ratio of the plot widget.
|
Lock aspect ratio of the plot widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def auto_range(self, value: "bool" = True):
|
||||||
|
"""
|
||||||
|
On demand apply autorange to the plot item based on the visible curves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(bool): If True, apply autorange to the visible curves.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def auto_range_x(self) -> "bool":
|
def auto_range_x(self) -> "bool":
|
||||||
@@ -2785,6 +2995,27 @@ class MotorMap(RPCBase):
|
|||||||
The font size of the legend font.
|
The font size of the legend font.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@minimal_crosshair_precision.setter
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def color(self) -> "tuple":
|
def color(self) -> "tuple":
|
||||||
@@ -2905,6 +3136,12 @@ class MotorMap(RPCBase):
|
|||||||
class MultiWaveform(RPCBase):
|
class MultiWaveform(RPCBase):
|
||||||
"""MultiWaveform widget for displaying multiple waveforms emitted by a single signal."""
|
"""MultiWaveform widget for displaying multiple waveforms emitted by a single signal."""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
Cleanup the BECConnector
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def enable_toolbar(self) -> "bool":
|
def enable_toolbar(self) -> "bool":
|
||||||
@@ -3106,6 +3343,15 @@ class MultiWaveform(RPCBase):
|
|||||||
Lock aspect ratio of the plot widget.
|
Lock aspect ratio of the plot widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def auto_range(self, value: "bool" = True):
|
||||||
|
"""
|
||||||
|
On demand apply autorange to the plot item based on the visible curves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(bool): If True, apply autorange to the visible curves.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def auto_range_x(self) -> "bool":
|
def auto_range_x(self) -> "bool":
|
||||||
@@ -3190,6 +3436,13 @@ class MultiWaveform(RPCBase):
|
|||||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def highlighted_index(self):
|
def highlighted_index(self):
|
||||||
@@ -3327,6 +3580,137 @@ class MultiWaveform(RPCBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PdfViewerWidget(RPCBase):
|
||||||
|
"""A widget to display PDF documents with toolbar controls."""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def load_pdf(self, file_path: str):
|
||||||
|
"""
|
||||||
|
Load a PDF file into the viewer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the PDF file to load.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def zoom_in(self):
|
||||||
|
"""
|
||||||
|
Zoom in the PDF view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def zoom_out(self):
|
||||||
|
"""
|
||||||
|
Zoom out the PDF view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def fit_to_width(self):
|
||||||
|
"""
|
||||||
|
Fit PDF to width.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def fit_to_page(self):
|
||||||
|
"""
|
||||||
|
Fit PDF to page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def reset_zoom(self):
|
||||||
|
"""
|
||||||
|
Reset zoom to 100% (1.0 factor).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def previous_page(self):
|
||||||
|
"""
|
||||||
|
Go to previous page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def next_page(self):
|
||||||
|
"""
|
||||||
|
Go to next page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def toggle_continuous_scroll(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Toggle between single page and continuous scroll mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
checked (bool): True to enable continuous scroll, False for single page mode.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def page_spacing(self):
|
||||||
|
"""
|
||||||
|
Get the spacing between pages in continuous scroll mode.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@page_spacing.setter
|
||||||
|
@rpc_call
|
||||||
|
def page_spacing(self):
|
||||||
|
"""
|
||||||
|
Get the spacing between pages in continuous scroll mode.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def side_margins(self):
|
||||||
|
"""
|
||||||
|
Get the horizontal margins (side spacing) around the PDF content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@side_margins.setter
|
||||||
|
@rpc_call
|
||||||
|
def side_margins(self):
|
||||||
|
"""
|
||||||
|
Get the horizontal margins (side spacing) around the PDF content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def go_to_first_page(self):
|
||||||
|
"""
|
||||||
|
Go to the first page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def go_to_last_page(self):
|
||||||
|
"""
|
||||||
|
Go to the last page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def jump_to_page(self, page_number: int):
|
||||||
|
"""
|
||||||
|
Jump to a specific page number (1-based index).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def current_page(self):
|
||||||
|
"""
|
||||||
|
Get the current page number (1-based index).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def current_file_path(self):
|
||||||
|
"""
|
||||||
|
Get the current PDF file path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@current_file_path.setter
|
||||||
|
@rpc_call
|
||||||
|
def current_file_path(self):
|
||||||
|
"""
|
||||||
|
Get the current PDF file path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PositionIndicator(RPCBase):
|
class PositionIndicator(RPCBase):
|
||||||
"""Display a position within a defined range, e.g. motor limits."""
|
"""Display a position within a defined range, e.g. motor limits."""
|
||||||
|
|
||||||
@@ -3404,6 +3788,13 @@ class PositionerBox(RPCBase):
|
|||||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PositionerBox2D(RPCBase):
|
class PositionerBox2D(RPCBase):
|
||||||
"""Simple Widget to control two positioners in box form"""
|
"""Simple Widget to control two positioners in box form"""
|
||||||
@@ -3426,6 +3817,41 @@ class PositionerBox2D(RPCBase):
|
|||||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def enable_controls_hor(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Persisted switch for horizontal control buttons (tweak/step).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@enable_controls_hor.setter
|
||||||
|
@rpc_call
|
||||||
|
def enable_controls_hor(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Persisted switch for horizontal control buttons (tweak/step).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def enable_controls_ver(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Persisted switch for vertical control buttons (tweak/step).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@enable_controls_ver.setter
|
||||||
|
@rpc_call
|
||||||
|
def enable_controls_ver(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Persisted switch for vertical control buttons (tweak/step).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PositionerControlLine(RPCBase):
|
class PositionerControlLine(RPCBase):
|
||||||
"""A widget that controls a single device."""
|
"""A widget that controls a single device."""
|
||||||
@@ -3439,6 +3865,13 @@ class PositionerControlLine(RPCBase):
|
|||||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PositionerGroup(RPCBase):
|
class PositionerGroup(RPCBase):
|
||||||
"""Simple Widget to control a positioner in box form"""
|
"""Simple Widget to control a positioner in box form"""
|
||||||
@@ -3538,8 +3971,8 @@ class RectangularROI(RPCBase):
|
|||||||
@rpc_call
|
@rpc_call
|
||||||
def get_coordinates(self, typed: "bool | None" = None) -> "dict | tuple":
|
def get_coordinates(self, typed: "bool | None" = None) -> "dict | tuple":
|
||||||
"""
|
"""
|
||||||
Returns the coordinates of a rectangle's corners. Supports returning them
|
Returns the coordinates of a rectangle's corners, rectangle center and dimensions.
|
||||||
as either a dictionary with descriptive keys or a tuple of coordinates.
|
Supports returning them as either a dictionary with descriptive keys or a tuple of coordinates.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
typed (bool | None): If True, returns coordinates as a dictionary with
|
typed (bool | None): If True, returns coordinates as a dictionary with
|
||||||
@@ -3547,7 +3980,7 @@ class RectangularROI(RPCBase):
|
|||||||
the value of `self.description`.
|
the value of `self.description`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict | tuple: The rectangle's corner coordinates, where the format
|
dict | tuple: The rectangle's corner coordinates, rectangle center and dimensions, where the format
|
||||||
depends on the `typed` parameter.
|
depends on the `typed` parameter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -3765,7 +4198,7 @@ class RingProgressBar(RPCBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@rpc_call
|
@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.
|
Set the precision for the progress bars. If bar_index is not provide, the precision will be set for all progress bars.
|
||||||
|
|
||||||
@@ -3897,6 +4330,13 @@ class ScanControl(RPCBase):
|
|||||||
Cleanup the BECConnector
|
Cleanup the BECConnector
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ScanProgressBar(RPCBase):
|
class ScanProgressBar(RPCBase):
|
||||||
"""Widget to display a progress bar that is hooked up to the scan progress of a scan."""
|
"""Widget to display a progress bar that is hooked up to the scan progress of a scan."""
|
||||||
@@ -3920,6 +4360,12 @@ class ScatterCurve(RPCBase):
|
|||||||
|
|
||||||
|
|
||||||
class ScatterWaveform(RPCBase):
|
class ScatterWaveform(RPCBase):
|
||||||
|
@rpc_call
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
Cleanup the BECConnector
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def enable_toolbar(self) -> "bool":
|
def enable_toolbar(self) -> "bool":
|
||||||
@@ -4121,6 +4567,15 @@ class ScatterWaveform(RPCBase):
|
|||||||
Lock aspect ratio of the plot widget.
|
Lock aspect ratio of the plot widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def auto_range(self, value: "bool" = True):
|
||||||
|
"""
|
||||||
|
On demand apply autorange to the plot item based on the visible curves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(bool): If True, apply autorange to the visible curves.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def auto_range_x(self) -> "bool":
|
def auto_range_x(self) -> "bool":
|
||||||
@@ -4205,6 +4660,13 @@ class ScatterWaveform(RPCBase):
|
|||||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def main_curve(self) -> "ScatterCurve":
|
def main_curve(self) -> "ScatterCurve":
|
||||||
@@ -4434,6 +4896,20 @@ class SignalLabel(RPCBase):
|
|||||||
Displays the full data from array signals if set to True.
|
Displays the full data from array signals if set to True.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def max_list_display_len(self) -> "int":
|
||||||
|
"""
|
||||||
|
For small lists, the max length to display
|
||||||
|
"""
|
||||||
|
|
||||||
|
@max_list_display_len.setter
|
||||||
|
@rpc_call
|
||||||
|
def max_list_display_len(self) -> "int":
|
||||||
|
"""
|
||||||
|
For small lists, the max length to display
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class SignalLineEdit(RPCBase):
|
class SignalLineEdit(RPCBase):
|
||||||
"""Line edit widget for device input with autocomplete for device names."""
|
"""Line edit widget for device input with autocomplete for device names."""
|
||||||
@@ -4509,14 +4985,6 @@ class TextBox(RPCBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class UILaunchWindow(RPCBase):
|
|
||||||
@rpc_call
|
|
||||||
def remove(self):
|
|
||||||
"""
|
|
||||||
Cleanup the BECConnector
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class VSCodeEditor(RPCBase):
|
class VSCodeEditor(RPCBase):
|
||||||
"""A widget to display the VSCode editor."""
|
"""A widget to display the VSCode editor."""
|
||||||
|
|
||||||
@@ -4526,14 +4994,10 @@ class VSCodeEditor(RPCBase):
|
|||||||
class Waveform(RPCBase):
|
class Waveform(RPCBase):
|
||||||
"""Widget for plotting waveforms."""
|
"""Widget for plotting waveforms."""
|
||||||
|
|
||||||
@property
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def _config_dict(self) -> "dict":
|
def remove(self):
|
||||||
"""
|
"""
|
||||||
Get the configuration of the widget.
|
Cleanup the BECConnector
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: The configuration of the widget.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -4737,6 +5201,15 @@ class Waveform(RPCBase):
|
|||||||
Lock aspect ratio of the plot widget.
|
Lock aspect ratio of the plot widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def auto_range(self, value: "bool" = True):
|
||||||
|
"""
|
||||||
|
On demand apply autorange to the plot item based on the visible curves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(bool): If True, apply autorange to the visible curves.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def auto_range_x(self) -> "bool":
|
def auto_range_x(self) -> "bool":
|
||||||
@@ -4765,15 +5238,6 @@ class Waveform(RPCBase):
|
|||||||
Set auto range for the y-axis.
|
Set auto range for the y-axis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@rpc_call
|
|
||||||
def auto_range(self, value: "bool" = True):
|
|
||||||
"""
|
|
||||||
On demand apply autorange to the plot item based on the visible curves.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value(bool): If True, apply autorange to the visible curves.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def x_log(self) -> "bool":
|
def x_log(self) -> "bool":
|
||||||
@@ -4830,6 +5294,23 @@ class Waveform(RPCBase):
|
|||||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_timeout(None)
|
||||||
|
@rpc_call
|
||||||
|
def screenshot(self, file_name: "str | None" = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def _config_dict(self) -> "dict":
|
||||||
|
"""
|
||||||
|
Get the configuration of the widget.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The configuration of the widget.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def curves(self) -> "list[Curve]":
|
def curves(self) -> "list[Curve]":
|
||||||
@@ -4937,6 +5418,8 @@ class Waveform(RPCBase):
|
|||||||
color: "str | None" = None,
|
color: "str | None" = None,
|
||||||
label: "str | None" = None,
|
label: "str | None" = None,
|
||||||
dap: "str | None" = None,
|
dap: "str | None" = None,
|
||||||
|
scan_id: "str | None" = None,
|
||||||
|
scan_number: "int | None" = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> "Curve":
|
) -> "Curve":
|
||||||
"""
|
"""
|
||||||
@@ -4956,9 +5439,13 @@ class Waveform(RPCBase):
|
|||||||
y_entry(str): The name of the entry for the y-axis.
|
y_entry(str): The name of the entry for the y-axis.
|
||||||
color(str): The color of the curve.
|
color(str): The color of the curve.
|
||||||
label(str): The label of the curve.
|
label(str): The label of the curve.
|
||||||
dap(str): The dap model to use for the curve, only available for sync devices.
|
dap(str): The dap model to use for the curve. When provided, a DAP curve is
|
||||||
If not specified, none will be added.
|
attached automatically for device, history, or custom data sources. Use
|
||||||
Use the same string as is the name of the LMFit model.
|
the same string as the LMFit model name.
|
||||||
|
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:
|
Returns:
|
||||||
Curve: The curve object.
|
Curve: The curve object.
|
||||||
@@ -4974,11 +5461,12 @@ class Waveform(RPCBase):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
) -> "Curve":
|
) -> "Curve":
|
||||||
"""
|
"""
|
||||||
Create a new DAP curve referencing the existing device curve `device_label`,
|
Create a new DAP curve referencing the existing curve `device_label`, with the
|
||||||
with the data processing model `dap_name`.
|
data processing model `dap_name`. DAP curves can be attached to curves that
|
||||||
|
originate from live devices, history, or fully custom data sources.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
device_label(str): The label of the device curve to add DAP to.
|
device_label(str): The label of the source curve to add DAP to.
|
||||||
dap_name(str): The name of the DAP model to use.
|
dap_name(str): The name of the DAP model to use.
|
||||||
color(str): The color of the curve.
|
color(str): The color of the curve.
|
||||||
dap_oversample(int): The oversampling factor for the DAP curve.
|
dap_oversample(int): The oversampling factor for the DAP curve.
|
||||||
@@ -5001,11 +5489,11 @@ class Waveform(RPCBase):
|
|||||||
def update_with_scan_history(self, scan_index: "int" = None, scan_id: "str" = None):
|
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.
|
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:
|
Args:
|
||||||
scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
|
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
|
@rpc_call
|
||||||
|
|||||||
@@ -14,18 +14,21 @@ from typing import TYPE_CHECKING, Literal, TypeAlias, cast
|
|||||||
|
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from bec_lib.utils.import_utils import lazy_import_from
|
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
import bec_widgets.cli.client as client
|
|
||||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
|
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
|
||||||
from bec_widgets.utils.serialization import register_serializer_extension
|
from bec_widgets.utils.serialization import register_serializer_extension
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from bec_lib.messages import GUIRegistryStateMessage
|
from bec_lib.messages import GUIRegistryStateMessage
|
||||||
|
|
||||||
|
import bec_widgets.cli.client as client
|
||||||
else:
|
else:
|
||||||
GUIRegistryStateMessage = lazy_import_from("bec_lib.messages", "GUIRegistryStateMessage")
|
GUIRegistryStateMessage = lazy_import_from("bec_lib.messages", "GUIRegistryStateMessage")
|
||||||
|
client = lazy_import("bec_widgets.cli.client")
|
||||||
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@@ -151,8 +154,10 @@ def wait_for_server(client: BECGuiClient):
|
|||||||
raise RuntimeError("GUI is not alive")
|
raise RuntimeError("GUI is not alive")
|
||||||
try:
|
try:
|
||||||
if client._gui_started_event.wait(timeout=timeout):
|
if client._gui_started_event.wait(timeout=timeout):
|
||||||
client._gui_started_timer.cancel()
|
if client._gui_started_timer is not None:
|
||||||
client._gui_started_timer.join()
|
# cancel the timer, we are done
|
||||||
|
client._gui_started_timer.cancel()
|
||||||
|
client._gui_started_timer.join()
|
||||||
else:
|
else:
|
||||||
raise TimeoutError("Could not connect to GUI server")
|
raise TimeoutError("Could not connect to GUI server")
|
||||||
finally:
|
finally:
|
||||||
@@ -261,18 +266,37 @@ class BECGuiClient(RPCBase):
|
|||||||
|
|
||||||
def start(self, wait: bool = False) -> None:
|
def start(self, wait: bool = False) -> None:
|
||||||
"""Start the GUI server."""
|
"""Start the GUI server."""
|
||||||
|
logger.warning("Using <gui>.start() is deprecated, use <gui>.show() instead.")
|
||||||
return self._start(wait=wait)
|
return self._start(wait=wait)
|
||||||
|
|
||||||
def show(self):
|
def show(self, wait=True) -> None:
|
||||||
"""Show the GUI window."""
|
"""
|
||||||
|
Show the GUI window.
|
||||||
|
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():
|
if self._check_if_server_is_alive():
|
||||||
return self._show_all()
|
return self._show_all()
|
||||||
return self.start(wait=True)
|
return self._start(wait=wait)
|
||||||
|
|
||||||
def hide(self):
|
def hide(self):
|
||||||
"""Hide the GUI window."""
|
"""Hide the GUI window."""
|
||||||
return self._hide_all()
|
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(
|
def new(
|
||||||
self,
|
self,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
@@ -382,6 +406,9 @@ class BECGuiClient(RPCBase):
|
|||||||
"""
|
"""
|
||||||
Start the GUI server, and execute callback when it is launched
|
Start the GUI server, and execute callback when it is launched
|
||||||
"""
|
"""
|
||||||
|
if self._gui_is_alive():
|
||||||
|
self._gui_started_event.set()
|
||||||
|
return
|
||||||
if self._process is None or self._process.poll() is not None:
|
if self._process is None or self._process.poll() is not None:
|
||||||
logger.success("GUI starting...")
|
logger.success("GUI starting...")
|
||||||
self._startup_timeout = 5
|
self._startup_timeout = 5
|
||||||
@@ -428,8 +455,8 @@ class BECGuiClient(RPCBase):
|
|||||||
self._update_dynamic_namespace(self._server_registry)
|
self._update_dynamic_namespace(self._server_registry)
|
||||||
|
|
||||||
def _do_show_all(self):
|
def _do_show_all(self):
|
||||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
if self.launcher and len(self._top_level) == 0:
|
||||||
rpc_client._run_rpc("show") # pylint: disable=protected-access
|
self.launcher._run_rpc("show") # pylint: disable=protected-access
|
||||||
for window in self._top_level.values():
|
for window in self._top_level.values():
|
||||||
window.show()
|
window.show()
|
||||||
|
|
||||||
@@ -439,11 +466,24 @@ class BECGuiClient(RPCBase):
|
|||||||
|
|
||||||
def _hide_all(self):
|
def _hide_all(self):
|
||||||
with wait_for_server(self):
|
with wait_for_server(self):
|
||||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
if self._killed:
|
||||||
rpc_client._run_rpc("hide") # pylint: disable=protected-access
|
return
|
||||||
if not self._killed:
|
self.launcher._run_rpc("hide")
|
||||||
for window in self._top_level.values():
|
for window in self._top_level.values():
|
||||||
window.hide()
|
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):
|
def _update_dynamic_namespace(self, server_registry: dict):
|
||||||
"""
|
"""
|
||||||
@@ -524,7 +564,7 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
# Test the client_utils.py module
|
# Test the client_utils.py module
|
||||||
gui = BECGuiClient()
|
gui = BECGuiClient()
|
||||||
|
|
||||||
gui.start(wait=True)
|
gui.show(wait=True)
|
||||||
gui.new().new(widget="Waveform")
|
gui.new().new(widget="Waveform")
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from __future__ import annotations
|
|||||||
{base_imports}
|
{base_imports}
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
|
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
|
||||||
{"from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module" if self._base else ""}
|
{"from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module" if self._base else ""}
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
@@ -180,7 +180,10 @@ class {class_name}(RPCBase):"""
|
|||||||
f"Method {method} not found in class {cls.__name__}. "
|
f"Method {method} not found in class {cls.__name__}. "
|
||||||
f"Please check the USER_ACCESS list."
|
f"Please check the USER_ACCESS list."
|
||||||
)
|
)
|
||||||
|
if hasattr(obj, "__rpc_timeout__"):
|
||||||
|
timeout = {"value": obj.__rpc_timeout__}
|
||||||
|
else:
|
||||||
|
timeout = {}
|
||||||
if isinstance(obj, (property, QtProperty)):
|
if isinstance(obj, (property, QtProperty)):
|
||||||
# for the cli, we can map qt properties to regular properties
|
# for the cli, we can map qt properties to regular properties
|
||||||
if is_property_setter:
|
if is_property_setter:
|
||||||
@@ -205,14 +208,26 @@ class {class_name}(RPCBase):"""
|
|||||||
def {method}{str(sig_overload)}: ...
|
def {method}{str(sig_overload)}: ...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.content += """
|
self.content += f"""
|
||||||
@rpc_call"""
|
{self._rpc_call(timeout)}"""
|
||||||
self.content += f"""
|
self.content += f"""
|
||||||
def {method}{str(sig)}:
|
def {method}{str(sig)}:
|
||||||
\"\"\"
|
\"\"\"
|
||||||
{doc}
|
{doc}
|
||||||
\"\"\""""
|
\"\"\""""
|
||||||
|
|
||||||
|
def _rpc_call(self, timeout_info: dict[str, float | None]):
|
||||||
|
"""
|
||||||
|
Decorator to mark a method as an RPC call.
|
||||||
|
This is used to generate the client code for the method.
|
||||||
|
"""
|
||||||
|
if not timeout_info:
|
||||||
|
return "@rpc_call"
|
||||||
|
timeout = timeout_info.get("value", None)
|
||||||
|
return f"""
|
||||||
|
@rpc_timeout({timeout})
|
||||||
|
@rpc_call"""
|
||||||
|
|
||||||
def write(self, file_name: str):
|
def write(self, file_name: str):
|
||||||
"""
|
"""
|
||||||
Write the content to a file, automatically formatted with black.
|
Write the content to a file, automatically formatted with black.
|
||||||
|
|||||||
@@ -39,6 +39,29 @@ def _transform_args_kwargs(args, kwargs) -> tuple[tuple, dict]:
|
|||||||
return tuple(_name_arg(arg) for arg in args), {k: _name_arg(v) for k, v in kwargs.items()}
|
return tuple(_name_arg(arg) for arg in args), {k: _name_arg(v) for k, v in kwargs.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def rpc_timeout(timeout):
|
||||||
|
"""
|
||||||
|
A decorator to set a timeout for an RPC call.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeout: The timeout in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The decorated function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
if "timeout" not in kwargs:
|
||||||
|
kwargs["timeout"] = timeout
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def rpc_call(func):
|
def rpc_call(func):
|
||||||
"""
|
"""
|
||||||
A decorator for calling a function on the server.
|
A decorator for calling a function on the server.
|
||||||
@@ -179,6 +202,11 @@ class RPCBase:
|
|||||||
parent = parent._parent
|
parent = parent._parent
|
||||||
return parent # type: ignore
|
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(
|
def _run_rpc(
|
||||||
self,
|
self,
|
||||||
method,
|
method,
|
||||||
@@ -202,6 +230,12 @@ class RPCBase:
|
|||||||
Returns:
|
Returns:
|
||||||
The result of the RPC call.
|
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())
|
request_id = str(uuid.uuid4())
|
||||||
rpc_msg = messages.GUIInstructionMessage(
|
rpc_msg = messages.GUIInstructionMessage(
|
||||||
action=method,
|
action=method,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|||||||
# "btn6": self.btn6,
|
# "btn6": self.btn6,
|
||||||
# "pb": self.pb,
|
# "pb": self.pb,
|
||||||
# "pi": self.pi,
|
# "pi": self.pi,
|
||||||
# "wf": self.wf,
|
"wf": self.wf,
|
||||||
# "scatter": self.scatter,
|
# "scatter": self.scatter,
|
||||||
# "scatter_mi": self.scatter,
|
# "scatter_mi": self.scatter,
|
||||||
# "mwf": self.mwf,
|
# "mwf": self.mwf,
|
||||||
@@ -105,12 +105,11 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|||||||
# self.btn5 = QPushButton("Button 5")
|
# self.btn5 = QPushButton("Button 5")
|
||||||
# self.btn6 = QPushButton("Button 6")
|
# self.btn6 = QPushButton("Button 6")
|
||||||
#
|
#
|
||||||
# fifth_tab = QWidget()
|
fifth_tab = QWidget()
|
||||||
# fifth_tab_layout = QVBoxLayout(fifth_tab)
|
fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||||
# self.wf = Waveform()
|
self.wf = Waveform()
|
||||||
# fifth_tab_layout.addWidget(self.wf)
|
fifth_tab_layout.addWidget(self.wf)
|
||||||
# tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||||
# tab_widget.setCurrentIndex(4)
|
|
||||||
#
|
#
|
||||||
sixth_tab = QWidget()
|
sixth_tab = QWidget()
|
||||||
sixth_tab_layout = QVBoxLayout(sixth_tab)
|
sixth_tab_layout = QVBoxLayout(sixth_tab)
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class FakePositioner(BECPositioner):
|
|||||||
def set_read_value(self, value):
|
def set_read_value(self, value):
|
||||||
self.read_value = value
|
self.read_value = value
|
||||||
|
|
||||||
def read(self):
|
def read(self, cached=False):
|
||||||
return self.signals
|
return self.signals
|
||||||
|
|
||||||
def set_limits(self, limits):
|
def set_limits(self, limits):
|
||||||
|
|||||||
@@ -161,8 +161,6 @@ class BECConnector:
|
|||||||
|
|
||||||
# 2) Enforce unique objectName among siblings with the same BECConnector parent
|
# 2) Enforce unique objectName among siblings with the same BECConnector parent
|
||||||
self.setParent(parent)
|
self.setParent(parent)
|
||||||
if isinstance(self.parent(), QObject) and hasattr(self, "cleanup"):
|
|
||||||
self.parent().destroyed.connect(self._run_cleanup_on_deleted_parent)
|
|
||||||
|
|
||||||
# Error popups
|
# Error popups
|
||||||
self.error_utility = ErrorPopupUtility()
|
self.error_utility = ErrorPopupUtility()
|
||||||
@@ -186,24 +184,6 @@ class BECConnector:
|
|||||||
except:
|
except:
|
||||||
logger.error(f"Error getting parent_id for {self.__class__.__name__}")
|
logger.error(f"Error getting parent_id for {self.__class__.__name__}")
|
||||||
|
|
||||||
def _run_cleanup_on_deleted_parent(self) -> None:
|
|
||||||
"""
|
|
||||||
Run cleanup on the deleted parent.
|
|
||||||
This method is called when the parent is deleted.
|
|
||||||
"""
|
|
||||||
if not hasattr(self, "cleanup"):
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
if not self._destroyed:
|
|
||||||
self.cleanup()
|
|
||||||
self._destroyed = True
|
|
||||||
except Exception:
|
|
||||||
content = traceback.format_exc()
|
|
||||||
logger.info(
|
|
||||||
"Failed to run cleanup on deleted parent. "
|
|
||||||
f"This is not necessarily an error as the parent may be deleted before the child and includes already a cleanup. The following exception was raised:\n{content}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def change_object_name(self, name: str) -> None:
|
def change_object_name(self, name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Change the object name of the widget. Unregister old name and register the new one.
|
Change the object name of the widget. Unregister old name and register the new one.
|
||||||
@@ -233,7 +213,7 @@ class BECConnector:
|
|||||||
- If there's a nearest BECConnector parent, only compare with children of that parent.
|
- 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.
|
- 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)
|
parent_bec = WidgetHierarchy._get_becwidget_ancestor(self)
|
||||||
|
|
||||||
if parent_bec:
|
if parent_bec:
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ def _loaded_submodules_from_specs(
|
|||||||
try:
|
try:
|
||||||
submodule.__loader__.exec_module(submodule)
|
submodule.__loader__.exec_module(submodule)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
exception_text = "".join(traceback.format_exception(e))
|
||||||
f"Error loading plugin {submodule}: \n{''.join(traceback.format_exception(e))}"
|
if "(most likely due to a circular import)" in exception_text:
|
||||||
)
|
logger.warning(f"Circular import encountered while loading {submodule}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Error loading plugin {submodule}: \n{exception_text}")
|
||||||
yield submodule
|
yield submodule
|
||||||
|
|
||||||
|
|
||||||
@@ -59,7 +61,8 @@ def _get_widgets_from_module(module: ModuleType) -> BECClassContainer:
|
|||||||
module,
|
module,
|
||||||
predicate=lambda item: inspect.isclass(item)
|
predicate=lambda item: inspect.isclass(item)
|
||||||
and issubclass(item, BECWidget)
|
and issubclass(item, BECWidget)
|
||||||
and item is not BECWidget,
|
and item is not BECWidget
|
||||||
|
and not item.__module__.startswith("bec_widgets"),
|
||||||
)
|
)
|
||||||
return BECClassContainer(
|
return BECClassContainer(
|
||||||
BECClassInfo(name=k, module=module.__name__, file=module.__loader__.get_filename(), obj=v)
|
BECClassInfo(name=k, module=module.__name__, file=module.__loader__.get_filename(), obj=v)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import copier
|
||||||
|
import typer
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_lib.plugin_helper import plugin_repo_path
|
||||||
|
from bec_lib.utils.plugin_manager._constants import ANSWER_KEYS
|
||||||
|
from bec_lib.utils.plugin_manager._util import existing_data, git_stage_files, make_commit
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_plugin_manager.edit_ui import open_and_watch_ui_editor
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
_app = typer.Typer(rich_markup_mode="rich")
|
||||||
|
|
||||||
|
|
||||||
|
def _commit_added_widget(repo: Path, name: str):
|
||||||
|
git_stage_files(repo, [".copier-answers.yml"])
|
||||||
|
git_stage_files(repo / repo.name / "bec_widgets" / "widgets" / name, [])
|
||||||
|
make_commit(repo, f"plugin-manager added new widget: {name}")
|
||||||
|
logger.info(f"Committing new widget {name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _widget_exists(widget_list: list[dict[str, str | bool]], name: str):
|
||||||
|
return name in [w["name"] for w in widget_list]
|
||||||
|
|
||||||
|
|
||||||
|
def _editor_cb(ctx: typer.Context, value: bool):
|
||||||
|
if value and not ctx.params["use_ui"]:
|
||||||
|
raise typer.BadParameter("Can only open the editor if creating a .ui file!")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
_bold_blue = "\033[34m\033[1m"
|
||||||
|
_off = "\033[0m"
|
||||||
|
_USE_UI_MSG = "Generate a .ui file for use in bec-designer."
|
||||||
|
_OPEN_DESIGNER_MSG = f"""This app can watch for changes and recompile them to a python file imported to the widget whenever it is saved.
|
||||||
|
To open this editor independently, you can use {_bold_blue}bec-plugin-manager edit-ui [widget_name]{_off}.
|
||||||
|
Open the created widget .ui file in bec-designer now?"""
|
||||||
|
|
||||||
|
|
||||||
|
@_app.command()
|
||||||
|
def widget(
|
||||||
|
name: Annotated[str, typer.Argument(help="Enter a name for your widget in snake_case")],
|
||||||
|
use_ui: Annotated[bool, typer.Option(prompt=_USE_UI_MSG, help=_USE_UI_MSG)] = True,
|
||||||
|
open_editor: Annotated[
|
||||||
|
bool, typer.Option(prompt=_OPEN_DESIGNER_MSG, help=_OPEN_DESIGNER_MSG, callback=_editor_cb)
|
||||||
|
] = True,
|
||||||
|
):
|
||||||
|
"""Create a new widget plugin with the given name.
|
||||||
|
|
||||||
|
If [bold white]use_ui[/bold white] is set, a bec-designer .ui file will also be created. If \
|
||||||
|
[bold white]open_editor[/bold white] is additionally set, the .ui file will be opened in \
|
||||||
|
bec-designer and the compiled python version will be updated when changes are made and saved."""
|
||||||
|
if (formatted_name := name.lower().replace("-", "_")) != name:
|
||||||
|
logger.warning(f"Adjusting widget name from {name} to {formatted_name}")
|
||||||
|
if not formatted_name.isidentifier():
|
||||||
|
logger.error(
|
||||||
|
f"{name} is not a valid name for a widget (even after converting to {formatted_name}) - please enter something in snake_case"
|
||||||
|
)
|
||||||
|
exit(-1)
|
||||||
|
logger.info(f"Adding new widget {formatted_name} to the template...")
|
||||||
|
try:
|
||||||
|
repo = Path(plugin_repo_path())
|
||||||
|
plugin_data = existing_data(repo, [ANSWER_KEYS.VERSION, ANSWER_KEYS.WIDGETS])
|
||||||
|
if _widget_exists(plugin_data[ANSWER_KEYS.WIDGETS], formatted_name):
|
||||||
|
logger.error(f"Widget {formatted_name} already exists!")
|
||||||
|
exit(-1)
|
||||||
|
plugin_data[ANSWER_KEYS.WIDGETS].append({"name": formatted_name, "use_ui": use_ui})
|
||||||
|
copier.run_update(
|
||||||
|
repo,
|
||||||
|
data=plugin_data,
|
||||||
|
defaults=True,
|
||||||
|
unsafe=True,
|
||||||
|
overwrite=True,
|
||||||
|
vcs_ref=plugin_data[ANSWER_KEYS.VERSION],
|
||||||
|
)
|
||||||
|
_commit_added_widget(repo, formatted_name)
|
||||||
|
except Exception:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
logger.error("exiting...")
|
||||||
|
exit(-1)
|
||||||
|
logger.success(f"Added widget {formatted_name}!")
|
||||||
|
if open_editor:
|
||||||
|
open_and_watch_ui_editor(formatted_name)
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path
|
||||||
|
from watchdog.events import (
|
||||||
|
DirCreatedEvent,
|
||||||
|
DirModifiedEvent,
|
||||||
|
DirMovedEvent,
|
||||||
|
FileCreatedEvent,
|
||||||
|
FileModifiedEvent,
|
||||||
|
FileMovedEvent,
|
||||||
|
FileSystemEvent,
|
||||||
|
FileSystemEventHandler,
|
||||||
|
)
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_designer import open_designer
|
||||||
|
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
|
||||||
|
from bec_widgets.utils.plugin_utils import get_custom_classes
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class RecompileHandler(FileSystemEventHandler):
|
||||||
|
def __init__(self, in_file: Path, out_file: Path) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.in_file = str(in_file)
|
||||||
|
self.out_file = str(out_file)
|
||||||
|
self._pyside_import_re = re.compile(r"from PySide6\.(.*) import ")
|
||||||
|
self._widget_import_re = re.compile(
|
||||||
|
r"^from ([a-zA-Z_]*) import ([a-zA-Z_]*)$", re.MULTILINE
|
||||||
|
)
|
||||||
|
self._widget_modules = {
|
||||||
|
c.name: c.module for c in (get_custom_classes("bec_widgets") + get_all_plugin_widgets())
|
||||||
|
}
|
||||||
|
|
||||||
|
def on_created(self, event: DirCreatedEvent | FileCreatedEvent) -> None:
|
||||||
|
self.recompile(event)
|
||||||
|
|
||||||
|
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
|
||||||
|
self.recompile(event)
|
||||||
|
|
||||||
|
def on_moved(self, event: DirMovedEvent | FileMovedEvent) -> None:
|
||||||
|
self.recompile(event)
|
||||||
|
|
||||||
|
def recompile(self, event: FileSystemEvent) -> None:
|
||||||
|
if event.src_path == self.in_file or event.dest_path == self.in_file:
|
||||||
|
self._recompile()
|
||||||
|
|
||||||
|
def _recompile(self):
|
||||||
|
logger.success(".ui file modified, recompiling...")
|
||||||
|
code = subprocess.call(
|
||||||
|
["pyside6-uic", "--absolute-imports", self.in_file, "-o", self.out_file]
|
||||||
|
)
|
||||||
|
logger.success(f"compilation exited with code {code}")
|
||||||
|
if code != 0:
|
||||||
|
return
|
||||||
|
self._add_comment_to_file()
|
||||||
|
logger.success("updating imports...")
|
||||||
|
self._update_imports()
|
||||||
|
logger.success("formatting...")
|
||||||
|
code = subprocess.call(
|
||||||
|
["black", "--line-length=100", "--skip-magic-trailing-comma", self.out_file]
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
logger.error(f"Error while running black on {self.out_file}, code: {code}")
|
||||||
|
return
|
||||||
|
code = subprocess.call(
|
||||||
|
[
|
||||||
|
"isort",
|
||||||
|
"--line-length=100",
|
||||||
|
"--profile=black",
|
||||||
|
"--multi-line=3",
|
||||||
|
"--trailing-comma",
|
||||||
|
self.out_file,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
logger.error(f"Error while running isort on {self.out_file}, code: {code}")
|
||||||
|
return
|
||||||
|
logger.success("done!")
|
||||||
|
|
||||||
|
def _add_comment_to_file(self):
|
||||||
|
with open(self.out_file, "r+") as f:
|
||||||
|
initial = f.read()
|
||||||
|
f.seek(0)
|
||||||
|
f.write(f"# Generated from {self.in_file} by bec-plugin-manager - do not edit! \n")
|
||||||
|
f.write(
|
||||||
|
"# Use 'bec-plugin-manager edit-ui [widget_name]' to make changes, and this file will be updated accordingly. \n\n"
|
||||||
|
)
|
||||||
|
f.write(initial)
|
||||||
|
|
||||||
|
def _update_imports(self):
|
||||||
|
with open(self.out_file, "r+") as f:
|
||||||
|
initial = f.read()
|
||||||
|
f.seek(0)
|
||||||
|
qtpy_imports = re.sub(
|
||||||
|
self._pyside_import_re, lambda ob: f"from qtpy.{ob.group(1)} import ", initial
|
||||||
|
)
|
||||||
|
print(self._widget_modules)
|
||||||
|
print(re.findall(self._widget_import_re, qtpy_imports))
|
||||||
|
widget_imports = re.sub(
|
||||||
|
self._widget_import_re,
|
||||||
|
lambda ob: (
|
||||||
|
f"from {module} import {ob.group(2)}"
|
||||||
|
if (module := self._widget_modules.get(ob.group(2))) is not None
|
||||||
|
else ob.group(1)
|
||||||
|
),
|
||||||
|
qtpy_imports,
|
||||||
|
)
|
||||||
|
f.write(widget_imports)
|
||||||
|
f.truncate()
|
||||||
|
|
||||||
|
|
||||||
|
def open_and_watch_ui_editor(widget_name: str):
|
||||||
|
logger.info(f"Opening the editor for {widget_name}, and watching")
|
||||||
|
repo = Path(plugin_repo_path())
|
||||||
|
widget_dir = repo / plugin_package_name() / "bec_widgets" / "widgets" / widget_name
|
||||||
|
ui_file = widget_dir / f"{widget_name}.ui"
|
||||||
|
ui_outfile = widget_dir / f"{widget_name}_ui.py"
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Opening the editor for {widget_name}, and watching {ui_file} for changes. Whenever you save the file, it will be recompiled to {ui_outfile}"
|
||||||
|
)
|
||||||
|
recompile_handler = RecompileHandler(ui_file, ui_outfile)
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(recompile_handler, str(ui_file.parent))
|
||||||
|
observer.start()
|
||||||
|
try:
|
||||||
|
open_designer([str(ui_file)])
|
||||||
|
finally:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
|
logger.info("Editing session ended, exiting...")
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import darkdetect
|
import darkdetect
|
||||||
|
import shiboken6
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from qtpy.QtCore import QObject, Slot
|
from qtpy.QtCore import QObject
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication, QFileDialog, QWidget
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||||
from bec_widgets.utils.colors import set_theme
|
from bec_widgets.utils.colors import set_theme
|
||||||
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.utils.rpc_decorator import rpc_timeout
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from bec_widgets.widgets.containers.dock import BECDock
|
from bec_widgets.widgets.containers.dock import BECDock
|
||||||
@@ -87,7 +91,7 @@ class BECWidget(BECConnector):
|
|||||||
theme = "dark"
|
theme = "dark"
|
||||||
self.apply_theme(theme)
|
self.apply_theme(theme)
|
||||||
|
|
||||||
@Slot(str)
|
@SafeSlot(str)
|
||||||
def apply_theme(self, theme: str):
|
def apply_theme(self, theme: str):
|
||||||
"""
|
"""
|
||||||
Apply the theme to the widget.
|
Apply the theme to the widget.
|
||||||
@@ -96,12 +100,43 @@ class BECWidget(BECConnector):
|
|||||||
theme(str, optional): The theme to be applied.
|
theme(str, optional): The theme to be applied.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
@SafeSlot(str)
|
||||||
|
@rpc_timeout(None)
|
||||||
|
def screenshot(self, file_name: str | None = None):
|
||||||
|
"""
|
||||||
|
Take a screenshot of the dock area and save it to a file.
|
||||||
|
"""
|
||||||
|
if not isinstance(self, QWidget):
|
||||||
|
logger.error("Cannot take screenshot of non-QWidget instance")
|
||||||
|
return
|
||||||
|
|
||||||
|
screenshot = self.grab()
|
||||||
|
if file_name is None:
|
||||||
|
file_name, _ = QFileDialog.getSaveFileName(
|
||||||
|
self,
|
||||||
|
"Save Screenshot",
|
||||||
|
f"bec_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png",
|
||||||
|
"PNG Files (*.png);;JPEG Files (*.jpg *.jpeg);;All Files (*)",
|
||||||
|
)
|
||||||
|
if not file_name:
|
||||||
|
return
|
||||||
|
screenshot.save(file_name)
|
||||||
|
logger.info(f"Screenshot saved to {file_name}")
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Cleanup the widget."""
|
"""Cleanup the widget."""
|
||||||
with RPCRegister.delayed_broadcast():
|
with RPCRegister.delayed_broadcast():
|
||||||
# All widgets need to call super().cleanup() in their cleanup method
|
# All widgets need to call super().cleanup() in their cleanup method
|
||||||
logger.info(f"Registry cleanup for widget {self.__class__.__name__}")
|
logger.info(f"Registry cleanup for widget {self.__class__.__name__}")
|
||||||
self.rpc_register.remove_rpc(self)
|
self.rpc_register.remove_rpc(self)
|
||||||
|
children = self.findChildren(BECWidget)
|
||||||
|
for child in children:
|
||||||
|
if not shiboken6.isValid(child):
|
||||||
|
# If the child is not valid, it means it has already been deleted
|
||||||
|
continue
|
||||||
|
child.close()
|
||||||
|
child.deleteLater()
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Wrap the close even to ensure the rpc_register is cleaned up."""
|
"""Wrap the close even to ensure the rpc_register is cleaned up."""
|
||||||
|
|||||||
@@ -259,12 +259,3 @@ class CompactPopupWidget(QWidget):
|
|||||||
@expand_popup.setter
|
@expand_popup.setter
|
||||||
def expand_popup(self, popup: bool):
|
def expand_popup(self, popup: bool):
|
||||||
self._expand_popup = popup
|
self._expand_popup = popup
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
# Called by Qt, on closing - since the children widgets can be
|
|
||||||
# BECWidgets, it is good to explicitely call 'close' on them,
|
|
||||||
# to ensure proper resources cleanup
|
|
||||||
for child in self.container.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
|
|
||||||
child.close()
|
|
||||||
|
|
||||||
super().closeEvent(event)
|
|
||||||
|
|||||||
@@ -209,8 +209,11 @@ class Crosshair(QObject):
|
|||||||
if self.highlighted_curve_index is not None and hasattr(self.plot_item, "visible_curves"):
|
if self.highlighted_curve_index is not None and hasattr(self.plot_item, "visible_curves"):
|
||||||
# Focus on the highlighted curve only
|
# Focus on the highlighted curve only
|
||||||
self.items = [self.plot_item.visible_curves[self.highlighted_curve_index]]
|
self.items = [self.plot_item.visible_curves[self.highlighted_curve_index]]
|
||||||
else:
|
elif hasattr(self.plot_item, "visible_items"): # PlotBase general case
|
||||||
# Handle all curves
|
# 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
|
self.items = self.plot_item.items
|
||||||
|
|
||||||
# Create or update markers
|
# Create or update markers
|
||||||
|
|||||||
@@ -81,10 +81,11 @@ class TypedForm(BECWidget, QWidget):
|
|||||||
|
|
||||||
self._form_grid_container = QWidget(parent=self)
|
self._form_grid_container = QWidget(parent=self)
|
||||||
self._form_grid_container.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
|
self._form_grid_container.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
|
||||||
|
self._form_grid_container.setLayout(QVBoxLayout())
|
||||||
|
self._layout.addWidget(self._form_grid_container)
|
||||||
|
|
||||||
self._form_grid = QWidget(parent=self._form_grid_container)
|
self._form_grid = QWidget(parent=self._form_grid_container)
|
||||||
self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
|
self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
|
||||||
self._layout.addWidget(self._form_grid_container)
|
|
||||||
self._form_grid_container.setLayout(QVBoxLayout())
|
|
||||||
self._form_grid.setLayout(self._new_grid_layout())
|
self._form_grid.setLayout(self._new_grid_layout())
|
||||||
|
|
||||||
self._widget_types: dict | None = None
|
self._widget_types: dict | None = None
|
||||||
@@ -105,11 +106,11 @@ class TypedForm(BECWidget, QWidget):
|
|||||||
|
|
||||||
def _add_griditem(self, item: FormItemSpec, row: int):
|
def _add_griditem(self, item: FormItemSpec, row: int):
|
||||||
grid = self._form_grid.layout()
|
grid = self._form_grid.layout()
|
||||||
label = QLabel(item.name)
|
label = QLabel(parent=self._form_grid, text=item.name)
|
||||||
label.setProperty("_model_field_name", item.name)
|
label.setProperty("_model_field_name", item.name)
|
||||||
label.setToolTip(item.info.description or item.name)
|
label.setToolTip(item.info.description or item.name)
|
||||||
grid.addWidget(label, row, 0)
|
grid.addWidget(label, row, 0)
|
||||||
widget = self._widget_from_type(item, self._widget_types)(parent=self, spec=item)
|
widget = self._widget_from_type(item, self._widget_types)(parent=self._form_grid, spec=item)
|
||||||
widget.valueChanged.connect(self.value_changed)
|
widget.valueChanged.connect(self.value_changed)
|
||||||
widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
|
widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
|
||||||
grid.addWidget(widget, row, 1)
|
grid.addWidget(widget, row, 1)
|
||||||
@@ -128,19 +129,17 @@ class TypedForm(BECWidget, QWidget):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _clear_grid(self):
|
def _clear_grid(self):
|
||||||
if (old_layout := self._form_grid.layout()) is not None:
|
gl = self._form_grid.layout()
|
||||||
while old_layout.count():
|
while w := gl.takeAt(0):
|
||||||
item = old_layout.takeAt(0)
|
w = w.widget()
|
||||||
widget = item.widget()
|
if hasattr(w, "teardown"):
|
||||||
if widget is not None:
|
w.teardown()
|
||||||
widget.deleteLater()
|
w.deleteLater()
|
||||||
old_layout.deleteLater()
|
self._form_grid_container.layout().removeWidget(self._form_grid)
|
||||||
self._form_grid.deleteLater()
|
self._form_grid.deleteLater()
|
||||||
self._form_grid = QWidget()
|
self._form_grid = QWidget()
|
||||||
self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
|
|
||||||
self._form_grid.setLayout(self._new_grid_layout())
|
self._form_grid.setLayout(self._new_grid_layout())
|
||||||
self._form_grid_container.layout().addWidget(self._form_grid)
|
self._form_grid_container.layout().addWidget(self._form_grid)
|
||||||
|
|
||||||
self.update_size()
|
self.update_size()
|
||||||
|
|
||||||
def update_size(self):
|
def update_size(self):
|
||||||
@@ -149,7 +148,7 @@ class TypedForm(BECWidget, QWidget):
|
|||||||
self.adjustSize()
|
self.adjustSize()
|
||||||
|
|
||||||
def _new_grid_layout(self):
|
def _new_grid_layout(self):
|
||||||
new_grid = QGridLayout()
|
new_grid = QGridLayout(self)
|
||||||
new_grid.setContentsMargins(0, 0, 0, 0)
|
new_grid.setContentsMargins(0, 0, 0, 0)
|
||||||
return new_grid
|
return new_grid
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,20 @@ from __future__ import annotations
|
|||||||
import typing
|
import typing
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from types import GenericAlias, UnionType
|
from types import GenericAlias, NoneType, UnionType
|
||||||
from typing import Callable, Final, Iterable, Literal, NamedTuple, OrderedDict, get_args
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Final,
|
||||||
|
Generic,
|
||||||
|
Iterable,
|
||||||
|
Literal,
|
||||||
|
NamedTuple,
|
||||||
|
Optional,
|
||||||
|
OrderedDict,
|
||||||
|
TypeVar,
|
||||||
|
get_args,
|
||||||
|
)
|
||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from bec_qthemes import material_icon
|
from bec_qthemes import material_icon
|
||||||
@@ -61,7 +73,7 @@ class FormItemSpec(BaseModel):
|
|||||||
|
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
item_type: type | UnionType | GenericAlias
|
item_type: type | UnionType | GenericAlias | Optional[Any]
|
||||||
name: str
|
name: str
|
||||||
info: FieldInfo = FieldInfo()
|
info: FieldInfo = FieldInfo()
|
||||||
pretty_display: bool = Field(
|
pretty_display: bool = Field(
|
||||||
@@ -178,6 +190,10 @@ class DynamicFormItem(QWidget):
|
|||||||
"""Add the main data entry widget to self._main_widget and appply any
|
"""Add the main data entry widget to self._main_widget and appply any
|
||||||
constraints from the field info"""
|
constraints from the field info"""
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def clear(self, *_):
|
||||||
|
return
|
||||||
|
|
||||||
def _set_pretty_display(self):
|
def _set_pretty_display(self):
|
||||||
self.setEnabled(False)
|
self.setEnabled(False)
|
||||||
if button := getattr(self, "_clear_button", None):
|
if button := getattr(self, "_clear_button", None):
|
||||||
@@ -194,11 +210,17 @@ class DynamicFormItem(QWidget):
|
|||||||
self._layout.addWidget(self._clear_button)
|
self._layout.addWidget(self._clear_button)
|
||||||
# the widget added in _add_main_widget must implement .clear() if value is not required
|
# the widget added in _add_main_widget must implement .clear() if value is not required
|
||||||
self._clear_button.setToolTip("Clear value or reset to default.")
|
self._clear_button.setToolTip("Clear value or reset to default.")
|
||||||
self._clear_button.clicked.connect(self._main_widget.clear) # type: ignore
|
self._clear_button.clicked.connect(self.clear) # type: ignore
|
||||||
|
|
||||||
def _value_changed(self, *_, **__):
|
def _value_changed(self, *_, **__):
|
||||||
self.valueChanged.emit()
|
self.valueChanged.emit()
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
self._layout.deleteLater()
|
||||||
|
self._layout.removeWidget(self._main_widget)
|
||||||
|
self._main_widget.deleteLater()
|
||||||
|
self._main_widget = None
|
||||||
|
|
||||||
|
|
||||||
class StrFormItem(DynamicFormItem):
|
class StrFormItem(DynamicFormItem):
|
||||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||||
@@ -350,11 +372,13 @@ class DictFormItem(DynamicFormItem):
|
|||||||
self._main_widget.replace_data(value)
|
self._main_widget.replace_data(value)
|
||||||
|
|
||||||
|
|
||||||
class _ItemAndWidgetType(NamedTuple):
|
_IW = TypeVar("_IW", bound=int | float | str)
|
||||||
# TODO: this should be generic but not supported in 3.10
|
|
||||||
item: type[int | float | str]
|
|
||||||
|
class _ItemAndWidgetType(NamedTuple, Generic[_IW]):
|
||||||
|
item: type[_IW]
|
||||||
widget: type[QWidget]
|
widget: type[QWidget]
|
||||||
default: int | float | str
|
default: _IW
|
||||||
|
|
||||||
|
|
||||||
class ListFormItem(DynamicFormItem):
|
class ListFormItem(DynamicFormItem):
|
||||||
@@ -530,11 +554,14 @@ class StrLiteralFormItem(DynamicFormItem):
|
|||||||
self._layout.addWidget(self._main_widget)
|
self._layout.addWidget(self._main_widget)
|
||||||
|
|
||||||
def getValue(self):
|
def getValue(self):
|
||||||
|
if self._main_widget.currentIndex() == -1:
|
||||||
|
return None
|
||||||
return self._main_widget.currentText()
|
return self._main_widget.currentText()
|
||||||
|
|
||||||
def setValue(self, value: str | None):
|
def setValue(self, value: str | None):
|
||||||
if value is None:
|
if value is None:
|
||||||
self.clear()
|
self.clear()
|
||||||
|
return
|
||||||
for i in range(self._main_widget.count()):
|
for i in range(self._main_widget.count()):
|
||||||
if self._main_widget.itemText(i) == value:
|
if self._main_widget.itemText(i) == value:
|
||||||
self._main_widget.setCurrentIndex(i)
|
self._main_widget.setCurrentIndex(i)
|
||||||
@@ -545,15 +572,39 @@ class StrLiteralFormItem(DynamicFormItem):
|
|||||||
self._main_widget.setCurrentIndex(-1)
|
self._main_widget.setCurrentIndex(-1)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionalStrLiteralFormItem(StrLiteralFormItem):
|
||||||
|
def _add_main_widget(self) -> None:
|
||||||
|
self._main_widget = QComboBox()
|
||||||
|
self._options = get_args(get_args(self._spec.info.annotation)[0])
|
||||||
|
for opt in self._options:
|
||||||
|
self._main_widget.addItem(opt)
|
||||||
|
self._layout.addWidget(self._main_widget)
|
||||||
|
|
||||||
|
|
||||||
WidgetTypeRegistry = OrderedDict[str, tuple[Callable[[FormItemSpec], bool], type[DynamicFormItem]]]
|
WidgetTypeRegistry = OrderedDict[str, tuple[Callable[[FormItemSpec], bool], type[DynamicFormItem]]]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_string_literal(t: type):
|
||||||
|
return type(t) is type(Literal[""]) and set(type(arg) for arg in get_args(t)) == {str}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_optional_string_literal(t: type):
|
||||||
|
if not hasattr(t, "__args__"):
|
||||||
|
return False
|
||||||
|
if len(t.__args__) != 2:
|
||||||
|
return False
|
||||||
|
if _is_string_literal(t.__args__[0]) and t.__args__[1] is NoneType:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_WIDGET_TYPES: Final[WidgetTypeRegistry] = OrderedDict() | {
|
DEFAULT_WIDGET_TYPES: Final[WidgetTypeRegistry] = OrderedDict() | {
|
||||||
# dict literals are ordered already but TypedForm subclasses may modify coppies of this dict
|
# dict literals are ordered already but TypedForm subclasses may modify coppies of this dict
|
||||||
# and delete/insert keys or change the order
|
# and delete/insert keys or change the order
|
||||||
"literal_str": (
|
"literal_str": (lambda spec: _is_string_literal(spec.info.annotation), StrLiteralFormItem),
|
||||||
lambda spec: type(spec.info.annotation) is type(Literal[""])
|
"optional_literal_str": (
|
||||||
and set(type(arg) for arg in get_args(spec.info.annotation)) == {str},
|
lambda spec: _is_optional_string_literal(spec.info.annotation),
|
||||||
StrLiteralFormItem,
|
OptionalStrLiteralFormItem,
|
||||||
),
|
),
|
||||||
"str": (lambda spec: spec.item_type in [str, str | None, None], StrFormItem),
|
"str": (lambda spec: spec.item_type in [str, str | None, None], StrFormItem),
|
||||||
"int": (lambda spec: spec.item_type in [int, int | None], IntFormItem),
|
"int": (lambda spec: spec.item_type in [int, int | None], IntFormItem),
|
||||||
@@ -604,6 +655,8 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
value5: int | None = Field()
|
value5: int | None = Field()
|
||||||
value6: list[int] = Field()
|
value6: list[int] = Field()
|
||||||
value7: list = Field()
|
value7: list = Field()
|
||||||
|
literal: Literal["a", "b", "c"]
|
||||||
|
nullable_literal: Literal["a", "b", "c"] | None = None
|
||||||
|
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
w = QWidget()
|
w = QWidget()
|
||||||
@@ -611,7 +664,7 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
w.setLayout(layout)
|
w.setLayout(layout)
|
||||||
items = []
|
items = []
|
||||||
for i, (field_name, info) in enumerate(TestModel.model_fields.items()):
|
for i, (field_name, info) in enumerate(TestModel.model_fields.items()):
|
||||||
spec = spec = FormItemSpec(item_type=info.annotation, name=field_name, info=info)
|
spec = FormItemSpec(item_type=info.annotation, name=field_name, info=info)
|
||||||
layout.addWidget(QLabel(field_name), i, 0)
|
layout.addWidget(QLabel(field_name), i, 0)
|
||||||
widg = widget_from_type(spec)(spec=spec)
|
widg = widget_from_type(spec)(spec=spec)
|
||||||
items.append(widg)
|
items.append(widg)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from qtpy.QtCore import QObject
|
|||||||
|
|
||||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||||
|
|
||||||
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
|
EXCLUDED_PLUGINS = ["BECConnector", "BECDock"]
|
||||||
_PARENT_ARG_REGEX = r".__init__\(\s*(?:parent\)|parent=parent,?|parent,?)"
|
_PARENT_ARG_REGEX = r".__init__\(\s*(?:parent\)|parent=parent,?|parent,?)"
|
||||||
_SELF_PARENT_ARG_REGEX = r".__init__\(\s*self,\s*(?:parent\)|parent=parent,?|parent,?)"
|
_SELF_PARENT_ARG_REGEX = r".__init__\(\s*self,\s*(?:parent\)|parent=parent,?|parent,?)"
|
||||||
SUPER_INIT_REGEX = re.compile(r"super\(\)" + _PARENT_ARG_REGEX, re.MULTILINE)
|
SUPER_INIT_REGEX = re.compile(r"super\(\)" + _PARENT_ARG_REGEX, re.MULTILINE)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
{widget_import}
|
{widget_import}
|
||||||
@@ -20,6 +21,8 @@ class {plugin_name_pascal}Plugin(QDesignerCustomWidgetInterface): # pragma: no
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = {plugin_name_pascal}(parent)
|
t = {plugin_name_pascal}(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ def get_custom_classes(repo_name: str) -> BECClassContainer:
|
|||||||
class_info = BECClassInfo(name=name, module=module.__name__, file=path, obj=obj)
|
class_info = BECClassInfo(name=name, module=module.__name__, file=path, obj=obj)
|
||||||
if issubclass(obj, BECConnector):
|
if issubclass(obj, BECConnector):
|
||||||
class_info.is_connector = True
|
class_info.is_connector = True
|
||||||
if issubclass(obj, BECWidget):
|
if issubclass(obj, QWidget) or issubclass(obj, BECWidget):
|
||||||
class_info.is_widget = True
|
class_info.is_widget = True
|
||||||
if len(subs) == 1 and (
|
if len(subs) == 1 and (
|
||||||
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
|
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
|
||||||
|
|||||||
@@ -0,0 +1,694 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from qtpy.QtCore import QLocale, QMetaEnum, Qt, QTimer
|
||||||
|
from qtpy.QtGui import QColor, QCursor, QFont, QIcon, QPalette
|
||||||
|
from qtpy.QtWidgets import (
|
||||||
|
QCheckBox,
|
||||||
|
QColorDialog,
|
||||||
|
QComboBox,
|
||||||
|
QDoubleSpinBox,
|
||||||
|
QFileDialog,
|
||||||
|
QFontDialog,
|
||||||
|
QHBoxLayout,
|
||||||
|
QHeaderView,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QMenu,
|
||||||
|
QPushButton,
|
||||||
|
QSizePolicy,
|
||||||
|
QSpinBox,
|
||||||
|
QToolButton,
|
||||||
|
QTreeWidget,
|
||||||
|
QTreeWidgetItem,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyEditor(QWidget):
|
||||||
|
def __init__(self, target: QWidget, parent: QWidget | None = None, show_only_bec: bool = True):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._target = target
|
||||||
|
self._bec_only = show_only_bec
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# Name row
|
||||||
|
name_row = QHBoxLayout()
|
||||||
|
name_row.addWidget(QLabel("Name:"))
|
||||||
|
self.name_edit = QLineEdit(target.objectName())
|
||||||
|
self.name_edit.setEnabled(False) # TODO implement with RPC broadcast
|
||||||
|
name_row.addWidget(self.name_edit)
|
||||||
|
layout.addLayout(name_row)
|
||||||
|
|
||||||
|
# BEC only checkbox
|
||||||
|
filter_row = QHBoxLayout()
|
||||||
|
self.chk_show_qt = QCheckBox("Show Qt properties")
|
||||||
|
self.chk_show_qt.setChecked(False)
|
||||||
|
filter_row.addWidget(self.chk_show_qt)
|
||||||
|
filter_row.addStretch(1)
|
||||||
|
layout.addLayout(filter_row)
|
||||||
|
self.chk_show_qt.toggled.connect(lambda checked: self.set_show_only_bec(not checked))
|
||||||
|
|
||||||
|
# Main tree widget
|
||||||
|
self.tree = QTreeWidget(self)
|
||||||
|
self.tree.setColumnCount(2)
|
||||||
|
self.tree.setHeaderLabels(["Property", "Value"])
|
||||||
|
self.tree.setAlternatingRowColors(True)
|
||||||
|
self.tree.setRootIsDecorated(False)
|
||||||
|
layout.addWidget(self.tree)
|
||||||
|
self._build()
|
||||||
|
|
||||||
|
def _class_chain(self):
|
||||||
|
chain = []
|
||||||
|
mo = self._target.metaObject()
|
||||||
|
while mo is not None:
|
||||||
|
chain.append(mo)
|
||||||
|
mo = mo.superClass()
|
||||||
|
return chain
|
||||||
|
|
||||||
|
def set_show_only_bec(self, flag: bool):
|
||||||
|
self._bec_only = flag
|
||||||
|
self._build()
|
||||||
|
|
||||||
|
def _set_equal_columns(self):
|
||||||
|
header = self.tree.header()
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.Interactive)
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.Interactive)
|
||||||
|
w = self.tree.viewport().width() or self.tree.width()
|
||||||
|
if w > 0:
|
||||||
|
half = max(1, w // 2)
|
||||||
|
self.tree.setColumnWidth(0, half)
|
||||||
|
self.tree.setColumnWidth(1, w - half)
|
||||||
|
|
||||||
|
def _build(self):
|
||||||
|
self.tree.clear()
|
||||||
|
for mo in self._class_chain():
|
||||||
|
class_name = mo.className()
|
||||||
|
if self._bec_only and not self._is_bec_metaobject(mo):
|
||||||
|
continue
|
||||||
|
group_item = QTreeWidgetItem(self.tree, [class_name])
|
||||||
|
group_item.setFirstColumnSpanned(True)
|
||||||
|
start = mo.propertyOffset()
|
||||||
|
end = mo.propertyCount()
|
||||||
|
for i in range(start, end):
|
||||||
|
prop = mo.property(i)
|
||||||
|
if (
|
||||||
|
not prop.isReadable()
|
||||||
|
or not prop.isWritable()
|
||||||
|
or not prop.isStored()
|
||||||
|
or not prop.isDesignable()
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
name = prop.name()
|
||||||
|
if name == "objectName":
|
||||||
|
continue
|
||||||
|
value = self._target.property(name)
|
||||||
|
self._add_property_row(group_item, name, value, prop)
|
||||||
|
if group_item.childCount() == 0:
|
||||||
|
idx = self.tree.indexOfTopLevelItem(group_item)
|
||||||
|
self.tree.takeTopLevelItem(idx)
|
||||||
|
self.tree.expandAll()
|
||||||
|
QTimer.singleShot(0, self._set_equal_columns)
|
||||||
|
|
||||||
|
def _enum_int(self, obj) -> int:
|
||||||
|
return int(getattr(obj, "value", obj))
|
||||||
|
|
||||||
|
def _make_sizepolicy_editor(self, name: str, sp):
|
||||||
|
if not isinstance(sp, QSizePolicy):
|
||||||
|
return None
|
||||||
|
wrap = QWidget(self)
|
||||||
|
row = QHBoxLayout(wrap)
|
||||||
|
row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
row.setSpacing(4)
|
||||||
|
h_combo = QComboBox(wrap)
|
||||||
|
v_combo = QComboBox(wrap)
|
||||||
|
hs = QSpinBox(wrap)
|
||||||
|
vs = QSpinBox(wrap)
|
||||||
|
for b in (hs, vs):
|
||||||
|
b.setRange(0, 16777215)
|
||||||
|
policies = [
|
||||||
|
(QSizePolicy.Fixed, "Fixed"),
|
||||||
|
(QSizePolicy.Minimum, "Minimum"),
|
||||||
|
(QSizePolicy.Maximum, "Maximum"),
|
||||||
|
(QSizePolicy.Preferred, "Preferred"),
|
||||||
|
(QSizePolicy.Expanding, "Expanding"),
|
||||||
|
(QSizePolicy.MinimumExpanding, "MinExpanding"),
|
||||||
|
(QSizePolicy.Ignored, "Ignored"),
|
||||||
|
]
|
||||||
|
for pol, text in policies:
|
||||||
|
h_combo.addItem(text, self._enum_int(pol))
|
||||||
|
v_combo.addItem(text, self._enum_int(pol))
|
||||||
|
|
||||||
|
def _set_current(combo, val):
|
||||||
|
idx = combo.findData(self._enum_int(val))
|
||||||
|
if idx >= 0:
|
||||||
|
combo.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
_set_current(h_combo, sp.horizontalPolicy())
|
||||||
|
_set_current(v_combo, sp.verticalPolicy())
|
||||||
|
hs.setValue(sp.horizontalStretch())
|
||||||
|
vs.setValue(sp.verticalStretch())
|
||||||
|
|
||||||
|
def apply_changes():
|
||||||
|
hp = QSizePolicy.Policy(h_combo.currentData())
|
||||||
|
vp = QSizePolicy.Policy(v_combo.currentData())
|
||||||
|
nsp = QSizePolicy(hp, vp)
|
||||||
|
nsp.setHorizontalStretch(hs.value())
|
||||||
|
nsp.setVerticalStretch(vs.value())
|
||||||
|
self._target.setProperty(name, nsp)
|
||||||
|
|
||||||
|
h_combo.currentIndexChanged.connect(lambda _=None: apply_changes())
|
||||||
|
v_combo.currentIndexChanged.connect(lambda _=None: apply_changes())
|
||||||
|
hs.valueChanged.connect(lambda _=None: apply_changes())
|
||||||
|
vs.valueChanged.connect(lambda _=None: apply_changes())
|
||||||
|
row.addWidget(h_combo)
|
||||||
|
row.addWidget(v_combo)
|
||||||
|
row.addWidget(hs)
|
||||||
|
row.addWidget(vs)
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
def _make_locale_editor(self, name: str, loc):
|
||||||
|
if not isinstance(loc, QLocale):
|
||||||
|
return None
|
||||||
|
wrap = QWidget(self)
|
||||||
|
row = QHBoxLayout(wrap)
|
||||||
|
row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
row.setSpacing(4)
|
||||||
|
lang_combo = QComboBox(wrap)
|
||||||
|
country_combo = QComboBox(wrap)
|
||||||
|
for lang in QLocale.Language:
|
||||||
|
try:
|
||||||
|
lang_int = self._enum_int(lang)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if lang_int < 0:
|
||||||
|
continue
|
||||||
|
name_txt = QLocale.languageToString(QLocale.Language(lang_int))
|
||||||
|
lang_combo.addItem(name_txt, lang_int)
|
||||||
|
|
||||||
|
def populate_countries():
|
||||||
|
country_combo.blockSignals(True)
|
||||||
|
country_combo.clear()
|
||||||
|
for terr in QLocale.Country:
|
||||||
|
try:
|
||||||
|
terr_int = self._enum_int(terr)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if terr_int < 0:
|
||||||
|
continue
|
||||||
|
text = QLocale.countryToString(QLocale.Country(terr_int))
|
||||||
|
country_combo.addItem(text, terr_int)
|
||||||
|
cur_country = self._enum_int(loc.country())
|
||||||
|
idx = country_combo.findData(cur_country)
|
||||||
|
if idx >= 0:
|
||||||
|
country_combo.setCurrentIndex(idx)
|
||||||
|
country_combo.blockSignals(False)
|
||||||
|
|
||||||
|
cur_lang = self._enum_int(loc.language())
|
||||||
|
idx = lang_combo.findData(cur_lang)
|
||||||
|
if idx >= 0:
|
||||||
|
lang_combo.setCurrentIndex(idx)
|
||||||
|
populate_countries()
|
||||||
|
|
||||||
|
def apply_locale():
|
||||||
|
lang = QLocale.Language(int(lang_combo.currentData()))
|
||||||
|
country = QLocale.Country(int(country_combo.currentData()))
|
||||||
|
self._target.setProperty(name, QLocale(lang, country))
|
||||||
|
|
||||||
|
lang_combo.currentIndexChanged.connect(lambda _=None: populate_countries())
|
||||||
|
lang_combo.currentIndexChanged.connect(lambda _=None: apply_locale())
|
||||||
|
country_combo.currentIndexChanged.connect(lambda _=None: apply_locale())
|
||||||
|
row.addWidget(lang_combo)
|
||||||
|
row.addWidget(country_combo)
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
def _make_icon_editor(self, name: str, icon):
|
||||||
|
btn = QPushButton(self)
|
||||||
|
btn.setText("Choose…")
|
||||||
|
if isinstance(icon, QIcon) and not icon.isNull():
|
||||||
|
btn.setIcon(icon)
|
||||||
|
|
||||||
|
def pick():
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, "Select Icon", "", "Images (*.png *.jpg *.jpeg *.bmp *.svg)"
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
ic = QIcon(path)
|
||||||
|
self._target.setProperty(name, ic)
|
||||||
|
btn.setIcon(ic)
|
||||||
|
|
||||||
|
btn.clicked.connect(pick)
|
||||||
|
return btn
|
||||||
|
|
||||||
|
def _spin_pair(self, ints: bool = True):
|
||||||
|
box1 = QSpinBox(self) if ints else QDoubleSpinBox(self)
|
||||||
|
box2 = QSpinBox(self) if ints else QDoubleSpinBox(self)
|
||||||
|
if ints:
|
||||||
|
box1.setRange(-10_000_000, 10_000_000)
|
||||||
|
box2.setRange(-10_000_000, 10_000_000)
|
||||||
|
else:
|
||||||
|
for b in (box1, box2):
|
||||||
|
b.setDecimals(6)
|
||||||
|
b.setRange(-1e12, 1e12)
|
||||||
|
b.setSingleStep(0.1)
|
||||||
|
row = QHBoxLayout()
|
||||||
|
row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
row.setSpacing(4)
|
||||||
|
wrap = QWidget(self)
|
||||||
|
wrap.setLayout(row)
|
||||||
|
row.addWidget(box1)
|
||||||
|
row.addWidget(box2)
|
||||||
|
return wrap, box1, box2
|
||||||
|
|
||||||
|
def _spin_quad(self, ints: bool = True):
|
||||||
|
s = QSpinBox if ints else QDoubleSpinBox
|
||||||
|
boxes = [s(self) for _ in range(4)]
|
||||||
|
if ints:
|
||||||
|
for b in boxes:
|
||||||
|
b.setRange(-10_000_000, 10_000_000)
|
||||||
|
else:
|
||||||
|
for b in boxes:
|
||||||
|
b.setDecimals(6)
|
||||||
|
b.setRange(-1e12, 1e12)
|
||||||
|
b.setSingleStep(0.1)
|
||||||
|
row = QHBoxLayout()
|
||||||
|
row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
row.setSpacing(4)
|
||||||
|
wrap = QWidget(self)
|
||||||
|
wrap.setLayout(row)
|
||||||
|
for b in boxes:
|
||||||
|
row.addWidget(b)
|
||||||
|
return wrap, boxes
|
||||||
|
|
||||||
|
def _make_font_editor(self, name: str, value):
|
||||||
|
btn = QPushButton(self)
|
||||||
|
if isinstance(value, QFont):
|
||||||
|
btn.setText(f"{value.family()}, {value.pointSize()}pt")
|
||||||
|
else:
|
||||||
|
btn.setText("Select font…")
|
||||||
|
|
||||||
|
def pick():
|
||||||
|
ok, font = QFontDialog.getFont(
|
||||||
|
value if isinstance(value, QFont) else QFont(), self, "Select Font"
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
self._target.setProperty(name, font)
|
||||||
|
btn.setText(f"{font.family()}, {font.pointSize()}pt")
|
||||||
|
|
||||||
|
btn.clicked.connect(pick)
|
||||||
|
return btn
|
||||||
|
|
||||||
|
def _make_color_editor(self, initial: QColor, apply_cb):
|
||||||
|
btn = QPushButton(self)
|
||||||
|
if isinstance(initial, QColor):
|
||||||
|
btn.setText(initial.name())
|
||||||
|
btn.setStyleSheet(f"background:{initial.name()};")
|
||||||
|
else:
|
||||||
|
btn.setText("Select color…")
|
||||||
|
|
||||||
|
def pick():
|
||||||
|
col = QColorDialog.getColor(
|
||||||
|
initial if isinstance(initial, QColor) else QColor(), self, "Select Color"
|
||||||
|
)
|
||||||
|
if col.isValid():
|
||||||
|
apply_cb(col)
|
||||||
|
btn.setText(col.name())
|
||||||
|
btn.setStyleSheet(f"background:{col.name()};")
|
||||||
|
|
||||||
|
btn.clicked.connect(pick)
|
||||||
|
return btn
|
||||||
|
|
||||||
|
def _apply_palette_color(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
pal: QPalette,
|
||||||
|
group: QPalette.ColorGroup,
|
||||||
|
role: QPalette.ColorRole,
|
||||||
|
col: QColor,
|
||||||
|
):
|
||||||
|
pal.setColor(group, role, col)
|
||||||
|
self._target.setProperty(name, pal)
|
||||||
|
|
||||||
|
def _make_palette_editor(self, name: str, pal: QPalette):
|
||||||
|
if not isinstance(pal, QPalette):
|
||||||
|
return None
|
||||||
|
wrap = QWidget(self)
|
||||||
|
row = QHBoxLayout(wrap)
|
||||||
|
row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
group_combo = QComboBox(wrap)
|
||||||
|
role_combo = QComboBox(wrap)
|
||||||
|
pick_btn = self._make_color_editor(
|
||||||
|
pal.color(QPalette.Active, QPalette.WindowText),
|
||||||
|
lambda col: self._apply_palette_color(
|
||||||
|
name, pal, QPalette.Active, QPalette.WindowText, col
|
||||||
|
),
|
||||||
|
)
|
||||||
|
groups = [
|
||||||
|
(QPalette.Active, "Active"),
|
||||||
|
(QPalette.Inactive, "Inactive"),
|
||||||
|
(QPalette.Disabled, "Disabled"),
|
||||||
|
]
|
||||||
|
for g, label in groups:
|
||||||
|
group_combo.addItem(label, int(getattr(g, "value", g)))
|
||||||
|
roles = [
|
||||||
|
(QPalette.WindowText, "WindowText"),
|
||||||
|
(QPalette.Window, "Window"),
|
||||||
|
(QPalette.Base, "Base"),
|
||||||
|
(QPalette.AlternateBase, "AlternateBase"),
|
||||||
|
(QPalette.ToolTipBase, "ToolTipBase"),
|
||||||
|
(QPalette.ToolTipText, "ToolTipText"),
|
||||||
|
(QPalette.Text, "Text"),
|
||||||
|
(QPalette.Button, "Button"),
|
||||||
|
(QPalette.ButtonText, "ButtonText"),
|
||||||
|
(QPalette.BrightText, "BrightText"),
|
||||||
|
(QPalette.Highlight, "Highlight"),
|
||||||
|
(QPalette.HighlightedText, "HighlightedText"),
|
||||||
|
]
|
||||||
|
for r, label in roles:
|
||||||
|
role_combo.addItem(label, int(getattr(r, "value", r)))
|
||||||
|
|
||||||
|
def rewire_button():
|
||||||
|
g = QPalette.ColorGroup(int(group_combo.currentData()))
|
||||||
|
r = QPalette.ColorRole(int(role_combo.currentData()))
|
||||||
|
col = pal.color(g, r)
|
||||||
|
while row.count() > 2:
|
||||||
|
w = row.takeAt(2).widget()
|
||||||
|
if w:
|
||||||
|
w.deleteLater()
|
||||||
|
btn = self._make_color_editor(
|
||||||
|
col, lambda c: self._apply_palette_color(name, pal, g, r, c)
|
||||||
|
)
|
||||||
|
row.addWidget(btn)
|
||||||
|
|
||||||
|
group_combo.currentIndexChanged.connect(lambda _: rewire_button())
|
||||||
|
role_combo.currentIndexChanged.connect(lambda _: rewire_button())
|
||||||
|
row.addWidget(group_combo)
|
||||||
|
row.addWidget(role_combo)
|
||||||
|
row.addWidget(pick_btn)
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
def _make_cursor_editor(self, name: str, value):
|
||||||
|
combo = QComboBox(self)
|
||||||
|
shapes = [
|
||||||
|
(Qt.ArrowCursor, "Arrow"),
|
||||||
|
(Qt.IBeamCursor, "IBeam"),
|
||||||
|
(Qt.WaitCursor, "Wait"),
|
||||||
|
(Qt.CrossCursor, "Cross"),
|
||||||
|
(Qt.UpArrowCursor, "UpArrow"),
|
||||||
|
(Qt.SizeAllCursor, "SizeAll"),
|
||||||
|
(Qt.PointingHandCursor, "PointingHand"),
|
||||||
|
(Qt.ForbiddenCursor, "Forbidden"),
|
||||||
|
(Qt.WhatsThisCursor, "WhatsThis"),
|
||||||
|
(Qt.BusyCursor, "Busy"),
|
||||||
|
]
|
||||||
|
current_shape = None
|
||||||
|
if isinstance(value, QCursor):
|
||||||
|
try:
|
||||||
|
enum_val = value.shape()
|
||||||
|
current_shape = int(getattr(enum_val, "value", enum_val))
|
||||||
|
except Exception:
|
||||||
|
current_shape = None
|
||||||
|
for shape, text in shapes:
|
||||||
|
combo.addItem(text, int(getattr(shape, "value", shape)))
|
||||||
|
if current_shape is not None:
|
||||||
|
idx = combo.findData(current_shape)
|
||||||
|
if idx >= 0:
|
||||||
|
combo.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
def apply_index(i):
|
||||||
|
shape_val = int(combo.itemData(i))
|
||||||
|
self._target.setProperty(name, QCursor(Qt.CursorShape(shape_val)))
|
||||||
|
|
||||||
|
combo.currentIndexChanged.connect(apply_index)
|
||||||
|
return combo
|
||||||
|
|
||||||
|
def _add_property_row(self, parent: QTreeWidgetItem, name: str, value, prop):
|
||||||
|
item = QTreeWidgetItem(parent, [name, ""])
|
||||||
|
editor = self._make_editor(name, value, prop)
|
||||||
|
if editor is not None:
|
||||||
|
self.tree.setItemWidget(item, 1, editor)
|
||||||
|
else:
|
||||||
|
item.setText(1, repr(value))
|
||||||
|
|
||||||
|
def _is_bec_metaobject(self, mo) -> bool:
|
||||||
|
cname = mo.className()
|
||||||
|
for cls in type(self._target).mro():
|
||||||
|
if getattr(cls, "__name__", None) == cname:
|
||||||
|
mod = getattr(cls, "__module__", "")
|
||||||
|
return mod.startswith("bec_widgets")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _enum_text(self, meta_enum: QMetaEnum, value_int: int) -> str:
|
||||||
|
if not meta_enum.isFlag():
|
||||||
|
key = meta_enum.valueToKey(value_int)
|
||||||
|
return key.decode() if isinstance(key, (bytes, bytearray)) else (key or str(value_int))
|
||||||
|
parts = []
|
||||||
|
for i in range(meta_enum.keyCount()):
|
||||||
|
k = meta_enum.key(i)
|
||||||
|
v = meta_enum.value(i)
|
||||||
|
if value_int & v:
|
||||||
|
k = k.decode() if isinstance(k, (bytes, bytearray)) else k
|
||||||
|
parts.append(k)
|
||||||
|
return " | ".join(parts) if parts else "0"
|
||||||
|
|
||||||
|
def _enum_value_to_int(self, meta_enum: QMetaEnum, value) -> int:
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
v = getattr(value, "value", None)
|
||||||
|
if isinstance(v, (int,)):
|
||||||
|
return int(v)
|
||||||
|
n = getattr(value, "name", None)
|
||||||
|
if isinstance(n, str):
|
||||||
|
res = meta_enum.keyToValue(n)
|
||||||
|
if res != -1:
|
||||||
|
return int(res)
|
||||||
|
s = str(value)
|
||||||
|
parts = [p.strip() for p in s.replace(",", "|").split("|")]
|
||||||
|
keys = []
|
||||||
|
for p in parts:
|
||||||
|
if "." in p:
|
||||||
|
p = p.split(".")[-1]
|
||||||
|
keys.append(p)
|
||||||
|
keystr = "|".join(keys)
|
||||||
|
try:
|
||||||
|
res = meta_enum.keysToValue(keystr)
|
||||||
|
if res != -1:
|
||||||
|
return int(res)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _make_enum_editor(self, name: str, value, prop):
|
||||||
|
meta_enum = prop.enumerator()
|
||||||
|
current = self._enum_value_to_int(meta_enum, value)
|
||||||
|
|
||||||
|
if not meta_enum.isFlag():
|
||||||
|
combo = QComboBox(self)
|
||||||
|
for i in range(meta_enum.keyCount()):
|
||||||
|
key = meta_enum.key(i)
|
||||||
|
key = key.decode() if isinstance(key, (bytes, bytearray)) else key
|
||||||
|
combo.addItem(key, meta_enum.value(i))
|
||||||
|
idx = combo.findData(current)
|
||||||
|
if idx < 0:
|
||||||
|
txt = self._enum_text(meta_enum, current)
|
||||||
|
idx = combo.findText(txt)
|
||||||
|
combo.setCurrentIndex(max(idx, 0))
|
||||||
|
|
||||||
|
def apply_index(i):
|
||||||
|
v = combo.itemData(i)
|
||||||
|
self._target.setProperty(name, int(v))
|
||||||
|
|
||||||
|
combo.currentIndexChanged.connect(apply_index)
|
||||||
|
return combo
|
||||||
|
|
||||||
|
btn = QToolButton(self)
|
||||||
|
btn.setText(self._enum_text(meta_enum, current))
|
||||||
|
btn.setPopupMode(QToolButton.InstantPopup)
|
||||||
|
menu = QMenu(btn)
|
||||||
|
actions = []
|
||||||
|
for i in range(meta_enum.keyCount()):
|
||||||
|
key = meta_enum.key(i)
|
||||||
|
key = key.decode() if isinstance(key, (bytes, bytearray)) else key
|
||||||
|
act = menu.addAction(key)
|
||||||
|
act.setCheckable(True)
|
||||||
|
act.setChecked(bool(current & meta_enum.value(i)))
|
||||||
|
actions.append(act)
|
||||||
|
btn.setMenu(menu)
|
||||||
|
|
||||||
|
def apply_flags():
|
||||||
|
flags = 0
|
||||||
|
for i, act in enumerate(actions):
|
||||||
|
if act.isChecked():
|
||||||
|
flags |= meta_enum.value(i)
|
||||||
|
self._target.setProperty(name, int(flags))
|
||||||
|
btn.setText(self._enum_text(meta_enum, flags))
|
||||||
|
|
||||||
|
menu.triggered.connect(lambda _a: apply_flags())
|
||||||
|
return btn
|
||||||
|
|
||||||
|
def _make_editor(self, name: str, value, prop):
|
||||||
|
from qtpy.QtCore import QPoint, QPointF, QRect, QRectF, QSize, QSizeF
|
||||||
|
|
||||||
|
if prop.isEnumType():
|
||||||
|
return self._make_enum_editor(name, value, prop)
|
||||||
|
if isinstance(value, QColor):
|
||||||
|
return self._make_color_editor(value, lambda col: self._target.setProperty(name, col))
|
||||||
|
if isinstance(value, QFont):
|
||||||
|
return self._make_font_editor(name, value)
|
||||||
|
if isinstance(value, QPalette):
|
||||||
|
return self._make_palette_editor(name, value)
|
||||||
|
if isinstance(value, QCursor):
|
||||||
|
return self._make_cursor_editor(name, value)
|
||||||
|
if isinstance(value, QSizePolicy):
|
||||||
|
ed = self._make_sizepolicy_editor(name, value)
|
||||||
|
if ed is not None:
|
||||||
|
return ed
|
||||||
|
if isinstance(value, QLocale):
|
||||||
|
ed = self._make_locale_editor(name, value)
|
||||||
|
if ed is not None:
|
||||||
|
return ed
|
||||||
|
if isinstance(value, QIcon):
|
||||||
|
ed = self._make_icon_editor(name, value)
|
||||||
|
if ed is not None:
|
||||||
|
return ed
|
||||||
|
if isinstance(value, QSize):
|
||||||
|
wrap, w, h = self._spin_pair(ints=True)
|
||||||
|
w.setValue(value.width())
|
||||||
|
h.setValue(value.height())
|
||||||
|
w.valueChanged.connect(
|
||||||
|
lambda _: self._target.setProperty(name, QSize(w.value(), h.value()))
|
||||||
|
)
|
||||||
|
h.valueChanged.connect(
|
||||||
|
lambda _: self._target.setProperty(name, QSize(w.value(), h.value()))
|
||||||
|
)
|
||||||
|
return wrap
|
||||||
|
if isinstance(value, QSizeF):
|
||||||
|
wrap, w, h = self._spin_pair(ints=False)
|
||||||
|
w.setValue(value.width())
|
||||||
|
h.setValue(value.height())
|
||||||
|
w.valueChanged.connect(
|
||||||
|
lambda _: self._target.setProperty(name, QSizeF(w.value(), h.value()))
|
||||||
|
)
|
||||||
|
h.valueChanged.connect(
|
||||||
|
lambda _: self._target.setProperty(name, QSizeF(w.value(), h.value()))
|
||||||
|
)
|
||||||
|
return wrap
|
||||||
|
if isinstance(value, QPoint):
|
||||||
|
wrap, x, y = self._spin_pair(ints=True)
|
||||||
|
x.setValue(value.x())
|
||||||
|
y.setValue(value.y())
|
||||||
|
x.valueChanged.connect(
|
||||||
|
lambda _: self._target.setProperty(name, QPoint(x.value(), y.value()))
|
||||||
|
)
|
||||||
|
y.valueChanged.connect(
|
||||||
|
lambda _: self._target.setProperty(name, QPoint(x.value(), y.value()))
|
||||||
|
)
|
||||||
|
return wrap
|
||||||
|
if isinstance(value, QPointF):
|
||||||
|
wrap, x, y = self._spin_pair(ints=False)
|
||||||
|
x.setValue(value.x())
|
||||||
|
y.setValue(value.y())
|
||||||
|
x.valueChanged.connect(
|
||||||
|
lambda _: self._target.setProperty(name, QPointF(x.value(), y.value()))
|
||||||
|
)
|
||||||
|
y.valueChanged.connect(
|
||||||
|
lambda _: self._target.setProperty(name, QPointF(x.value(), y.value()))
|
||||||
|
)
|
||||||
|
return wrap
|
||||||
|
if isinstance(value, QRect):
|
||||||
|
wrap, boxes = self._spin_quad(ints=True)
|
||||||
|
for b, v in zip(boxes, (value.x(), value.y(), value.width(), value.height())):
|
||||||
|
b.setValue(v)
|
||||||
|
|
||||||
|
def apply_rect():
|
||||||
|
self._target.setProperty(
|
||||||
|
name,
|
||||||
|
QRect(boxes[0].value(), boxes[1].value(), boxes[2].value(), boxes[3].value()),
|
||||||
|
)
|
||||||
|
|
||||||
|
for b in boxes:
|
||||||
|
b.valueChanged.connect(lambda _=None: apply_rect())
|
||||||
|
return wrap
|
||||||
|
if isinstance(value, QRectF):
|
||||||
|
wrap, boxes = self._spin_quad(ints=False)
|
||||||
|
for b, v in zip(boxes, (value.x(), value.y(), value.width(), value.height())):
|
||||||
|
b.setValue(v)
|
||||||
|
|
||||||
|
def apply_rectf():
|
||||||
|
self._target.setProperty(
|
||||||
|
name,
|
||||||
|
QRectF(boxes[0].value(), boxes[1].value(), boxes[2].value(), boxes[3].value()),
|
||||||
|
)
|
||||||
|
|
||||||
|
for b in boxes:
|
||||||
|
b.valueChanged.connect(lambda _=None: apply_rectf())
|
||||||
|
return wrap
|
||||||
|
if isinstance(value, bool):
|
||||||
|
w = QCheckBox(self)
|
||||||
|
w.setChecked(bool(value))
|
||||||
|
w.toggled.connect(lambda v: self._target.setProperty(name, v))
|
||||||
|
return w
|
||||||
|
if isinstance(value, int) and not isinstance(value, bool):
|
||||||
|
w = QSpinBox(self)
|
||||||
|
w.setRange(-10_000_000, 10_000_000)
|
||||||
|
w.setValue(int(value))
|
||||||
|
w.valueChanged.connect(lambda v: self._target.setProperty(name, v))
|
||||||
|
return w
|
||||||
|
if isinstance(value, float):
|
||||||
|
w = QDoubleSpinBox(self)
|
||||||
|
w.setDecimals(6)
|
||||||
|
w.setRange(-1e12, 1e12)
|
||||||
|
w.setSingleStep(0.1)
|
||||||
|
w.setValue(float(value))
|
||||||
|
w.valueChanged.connect(lambda v: self._target.setProperty(name, v))
|
||||||
|
return w
|
||||||
|
if isinstance(value, str):
|
||||||
|
w = QLineEdit(self)
|
||||||
|
w.setText(value)
|
||||||
|
w.editingFinished.connect(lambda: self._target.setProperty(name, w.text()))
|
||||||
|
return w
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DemoApp(QWidget): # pragma: no cover:
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
layout = QHBoxLayout(self)
|
||||||
|
|
||||||
|
# Create a BECWidget instance example
|
||||||
|
waveform = self.create_waveform()
|
||||||
|
|
||||||
|
# property editor for the BECWidget
|
||||||
|
property_editor = PropertyEditor(waveform, show_only_bec=True)
|
||||||
|
|
||||||
|
layout.addWidget(waveform)
|
||||||
|
layout.addWidget(property_editor)
|
||||||
|
|
||||||
|
def create_waveform(self):
|
||||||
|
"""Create a new waveform widget."""
|
||||||
|
|
||||||
|
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||||
|
|
||||||
|
waveform = Waveform(parent=self)
|
||||||
|
waveform.title = "New Waveform"
|
||||||
|
waveform.x_label = "X Axis"
|
||||||
|
waveform.y_label = "Y Axis"
|
||||||
|
return waveform
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover:
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
demo = DemoApp()
|
||||||
|
demo.setWindowTitle("Property Editor Demo")
|
||||||
|
demo.resize(1200, 800)
|
||||||
|
demo.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
@@ -13,3 +13,17 @@ def register_rpc_methods(cls):
|
|||||||
if getattr(method, "rpc_public", False):
|
if getattr(method, "rpc_public", False):
|
||||||
cls.USER_ACCESS.add(name)
|
cls.USER_ACCESS.add(name)
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
def rpc_timeout(timeout: float | None):
|
||||||
|
"""
|
||||||
|
Decorator to set a timeout for RPC methods.
|
||||||
|
The actual implementation of timeout handling is within the cli module. This decorator
|
||||||
|
is solely to inform the generate-cli command about the timeout value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
func.__rpc_timeout__ = timeout # Store the timeout value in the function
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
@@ -10,7 +11,7 @@ from bec_lib.client import BECClient
|
|||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from bec_lib.utils.import_utils import lazy_import
|
from bec_lib.utils.import_utils import lazy_import
|
||||||
from qtpy.QtCore import QTimer
|
from qtpy.QtCore import Qt, QTimer
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
from redis.exceptions import RedisError
|
from redis.exceptions import RedisError
|
||||||
|
|
||||||
@@ -128,16 +129,44 @@ class RPCServer:
|
|||||||
# Run with rpc registry broadcast, but only once
|
# Run with rpc registry broadcast, but only once
|
||||||
with RPCRegister.delayed_broadcast():
|
with RPCRegister.delayed_broadcast():
|
||||||
logger.debug(f"Running RPC instruction: {method} with args: {args}, kwargs: {kwargs}")
|
logger.debug(f"Running RPC instruction: {method} with args: {args}, kwargs: {kwargs}")
|
||||||
method_obj = getattr(obj, method)
|
if method == "raise" and hasattr(
|
||||||
# check if the method accepts args and kwargs
|
obj, "setWindowState"
|
||||||
if not callable(method_obj):
|
): # special case for raising windows, should work even if minimized
|
||||||
if not args:
|
# this is a special case for raising windows for gnome on rethat 9 systems where changing focus is supressed by default
|
||||||
res = method_obj
|
# The procedure is as follows:
|
||||||
else:
|
# 1. Get the current window state to check if the window is minimized and remove minimized flag
|
||||||
setattr(obj, method, args[0])
|
# 2. Then in order to force gnome to raise the window, we set the window to stay on top temporarily
|
||||||
res = None
|
# and call raise_() and activateWindow()
|
||||||
|
# This forces gnome to raise the window even if focus stealing is prevented
|
||||||
|
# 3. Flag for stay on top is removed again to restore the original window state
|
||||||
|
# 4. Finally, we call show() to ensure the window is visible
|
||||||
|
|
||||||
|
state = getattr(obj, "windowState", lambda: Qt.WindowNoState)()
|
||||||
|
target_state = state | Qt.WindowActive
|
||||||
|
if state & Qt.WindowMinimized:
|
||||||
|
target_state &= ~Qt.WindowMinimized
|
||||||
|
obj.setWindowState(target_state)
|
||||||
|
if hasattr(obj, "showNormal") and state & Qt.WindowMinimized:
|
||||||
|
obj.showNormal()
|
||||||
|
if hasattr(obj, "raise_"):
|
||||||
|
obj.setWindowFlags(obj.windowFlags() | Qt.WindowStaysOnTopHint)
|
||||||
|
obj.raise_()
|
||||||
|
if hasattr(obj, "activateWindow"):
|
||||||
|
obj.activateWindow()
|
||||||
|
obj.setWindowFlags(obj.windowFlags() & ~Qt.WindowStaysOnTopHint)
|
||||||
|
obj.show()
|
||||||
|
res = None
|
||||||
else:
|
else:
|
||||||
res = method_obj(*args, **kwargs)
|
method_obj = getattr(obj, method)
|
||||||
|
# check if the method accepts args and kwargs
|
||||||
|
if not callable(method_obj):
|
||||||
|
if not args:
|
||||||
|
res = method_obj
|
||||||
|
else:
|
||||||
|
setattr(obj, method, args[0])
|
||||||
|
res = None
|
||||||
|
else:
|
||||||
|
res = method_obj(*args, **kwargs)
|
||||||
|
|
||||||
if isinstance(res, list):
|
if isinstance(res, list):
|
||||||
res = [self.serialize_object(obj) for obj in res]
|
res = [self.serialize_object(obj) for obj in res]
|
||||||
@@ -229,6 +258,8 @@ class RPCServer:
|
|||||||
if wait:
|
if wait:
|
||||||
while not self.rpc_register.object_is_registered(connector):
|
while not self.rpc_register.object_is_registered(connector):
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
logger.info(f"Waiting for {connector} to be registered...")
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
widget_class = getattr(connector, "rpc_widget_class", None)
|
widget_class = getattr(connector, "rpc_widget_class", None)
|
||||||
if not widget_class:
|
if not widget_class:
|
||||||
|
|||||||
@@ -1,44 +1,25 @@
|
|||||||
|
from bec_lib.codecs import BECCodec
|
||||||
from bec_lib.serialization import msgpack
|
from bec_lib.serialization import msgpack
|
||||||
from qtpy.QtCore import QPointF
|
from qtpy.QtCore import QPointF
|
||||||
|
|
||||||
|
|
||||||
|
class QPointFEncoder(BECCodec):
|
||||||
|
obj_type = QPointF
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode(obj: QPointF) -> list[float]:
|
||||||
|
"""Encode a QPointF object to a list of floats."""
|
||||||
|
return [obj.x(), obj.y()]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode(type_name: str, data: list[float]) -> list[float]:
|
||||||
|
"""No-op function since QPointF is encoded as a list of floats."""
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def register_serializer_extension():
|
def register_serializer_extension():
|
||||||
"""
|
"""
|
||||||
Register the serializer extension for the BECConnector.
|
Register the serializer extension for the BECConnector.
|
||||||
"""
|
"""
|
||||||
if not module_is_registered("bec_widgets.utils.serialization"):
|
if not msgpack.is_registered(QPointF):
|
||||||
msgpack.register_object_hook(encode_qpointf, decode_qpointf)
|
msgpack.register(QPointF, QPointFEncoder.encode, QPointFEncoder.decode)
|
||||||
|
|
||||||
|
|
||||||
def module_is_registered(module_name: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if the module is registered in the encoder.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
module_name (str): The name of the module to check.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the module is registered, False otherwise.
|
|
||||||
"""
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
for enc in msgpack._encoder:
|
|
||||||
if enc[0].__module__ == module_name:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def encode_qpointf(obj):
|
|
||||||
"""
|
|
||||||
Encode a QPointF object to a list of floats. As this is mostly used for sending
|
|
||||||
data to the client, it is not necessary to convert it back to a QPointF object.
|
|
||||||
"""
|
|
||||||
if isinstance(obj, QPointF):
|
|
||||||
return [obj.x(), obj.y()]
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def decode_qpointf(obj):
|
|
||||||
"""
|
|
||||||
no-op function since QPointF is encoded as a list of floats.
|
|
||||||
"""
|
|
||||||
return obj
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class AutoUpdates(BECMainWindow):
|
|||||||
_default_dock: BECDock
|
_default_dock: BECDock
|
||||||
USER_ACCESS = ["enabled", "enabled.setter", "selected_device", "selected_device.setter"]
|
USER_ACCESS = ["enabled", "enabled.setter", "selected_device", "selected_device.setter"]
|
||||||
RPC = True
|
RPC = True
|
||||||
|
PLUGIN = False
|
||||||
|
|
||||||
# enforce that subclasses have the same rpc widget class
|
# enforce that subclasses have the same rpc widget class
|
||||||
rpc_widget_class = "AutoUpdates"
|
rpc_widget_class = "AutoUpdates"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{'files': ['dock_area.py']}
|
||||||
+8
-9
@@ -1,22 +1,19 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
import bec_widgets
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||||
|
|
||||||
DOM_XML = """
|
DOM_XML = """
|
||||||
<ui language='c++'>
|
<ui language='c++'>
|
||||||
<widget class='BECDockArea' name='dock_area'>
|
<widget class='BECDockArea' name='bec_dock_area'>
|
||||||
</widget>
|
</widget>
|
||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -24,6 +21,8 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = BECDockArea(parent)
|
t = BECDockArea(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -31,13 +30,13 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "BEC Plots"
|
return "BEC Containers"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(BECDockArea.ICON_NAME)
|
return designer_material_icon(BECDockArea.ICON_NAME)
|
||||||
|
|
||||||
def includeFile(self):
|
def includeFile(self):
|
||||||
return "dock_area"
|
return "bec_dock_area"
|
||||||
|
|
||||||
def initialize(self, form_editor):
|
def initialize(self, form_editor):
|
||||||
self._form_editor = form_editor
|
self._form_editor = form_editor
|
||||||
@@ -52,7 +51,7 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "BECDockArea"
|
return "BECDockArea"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "BECDockArea"
|
return ""
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
@@ -389,6 +389,7 @@ class BECDock(BECWidget, Dock):
|
|||||||
if widget in self.widgets:
|
if widget in self.widgets:
|
||||||
self.widgets.remove(widget)
|
self.widgets.remove(widget)
|
||||||
widget.close()
|
widget.close()
|
||||||
|
widget.deleteLater()
|
||||||
|
|
||||||
def delete_all(self):
|
def delete_all(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class BECDockArea(BECWidget, QWidget):
|
|||||||
"detach_dock",
|
"detach_dock",
|
||||||
"attach_all",
|
"attach_all",
|
||||||
"save_state",
|
"save_state",
|
||||||
|
"screenshot",
|
||||||
"restore_state",
|
"restore_state",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -267,11 +268,16 @@ class BECDockArea(BECWidget, QWidget):
|
|||||||
"restore_state",
|
"restore_state",
|
||||||
MaterialIconAction(icon_name="frame_reload", tooltip="Restore Dock State", parent=self),
|
MaterialIconAction(icon_name="frame_reload", tooltip="Restore Dock State", parent=self),
|
||||||
)
|
)
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"screenshot",
|
||||||
|
MaterialIconAction(icon_name="photo_camera", tooltip="Take Screenshot", parent=self),
|
||||||
|
)
|
||||||
|
|
||||||
bundle = ToolbarBundle("dock_actions", self.toolbar.components)
|
bundle = ToolbarBundle("dock_actions", self.toolbar.components)
|
||||||
bundle.add_action("attach_all")
|
bundle.add_action("attach_all")
|
||||||
bundle.add_action("save_state")
|
bundle.add_action("save_state")
|
||||||
bundle.add_action("restore_state")
|
bundle.add_action("restore_state")
|
||||||
|
bundle.add_action("screenshot")
|
||||||
self.toolbar.add_bundle(bundle)
|
self.toolbar.add_bundle(bundle)
|
||||||
|
|
||||||
def _hook_toolbar(self):
|
def _hook_toolbar(self):
|
||||||
@@ -333,6 +339,7 @@ class BECDockArea(BECWidget, QWidget):
|
|||||||
self.toolbar.components.get_action("restore_state").action.triggered.connect(
|
self.toolbar.components.get_action("restore_state").action.triggered.connect(
|
||||||
self.restore_state
|
self.restore_state
|
||||||
)
|
)
|
||||||
|
self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot)
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def _create_widget_from_toolbar(self, widget_name: str) -> None:
|
def _create_widget_from_toolbar(self, widget_name: str) -> None:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{'files': ['dock_area.py','dock.py']}
|
|
||||||
+1
-1
@@ -6,7 +6,7 @@ def main(): # pragma: no cover
|
|||||||
return
|
return
|
||||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
from bec_widgets.widgets.containers.dock.dock_area_plugin import BECDockAreaPlugin
|
from bec_widgets.widgets.containers.dock.bec_dock_area_plugin import BECDockAreaPlugin
|
||||||
|
|
||||||
QPyDesignerCustomWidgetCollection.addCustomWidget(BECDockAreaPlugin())
|
QPyDesignerCustomWidgetCollection.addCustomWidget(BECDockAreaPlugin())
|
||||||
|
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from bec_qthemes import material_icon
|
||||||
|
from qtpy.QtCore import QMimeData, Qt, Signal
|
||||||
|
from qtpy.QtGui import QDrag
|
||||||
|
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QSizePolicy, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.colors import get_theme_palette
|
||||||
|
from bec_widgets.utils.error_popups import SafeProperty
|
||||||
|
|
||||||
|
|
||||||
|
class CollapsibleSection(QWidget):
|
||||||
|
"""A widget that combines a header button with any content widget for collapsible sections
|
||||||
|
|
||||||
|
This widget contains a header button with a title and a content widget.
|
||||||
|
The content widget can be any QWidget. The header button can be expanded or collapsed.
|
||||||
|
The header also contains an "Add" button that is only visible when hovering over the section.
|
||||||
|
|
||||||
|
Signals:
|
||||||
|
section_reorder_requested(str, str): Emitted when the section is dragged and dropped
|
||||||
|
onto another section for reordering.
|
||||||
|
Arguments are (source_title, target_title).
|
||||||
|
"""
|
||||||
|
|
||||||
|
section_reorder_requested = Signal(str, str) # (source_title, target_title)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, title="", indentation=10, show_add_button=False):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self.title = title
|
||||||
|
self.content_widget = None
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self._expanded = True
|
||||||
|
|
||||||
|
# Setup layout
|
||||||
|
self.main_layout = QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(indentation, 0, 0, 0)
|
||||||
|
self.main_layout.setSpacing(0)
|
||||||
|
|
||||||
|
header_layout = QHBoxLayout()
|
||||||
|
header_layout.setContentsMargins(0, 0, 4, 0)
|
||||||
|
header_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Create header button
|
||||||
|
self.header_button = QPushButton()
|
||||||
|
self.header_button.clicked.connect(self.toggle_expanded)
|
||||||
|
|
||||||
|
# Enable drag and drop for reordering
|
||||||
|
self.header_button.setAcceptDrops(True)
|
||||||
|
self.header_button.mousePressEvent = self._header_mouse_press_event
|
||||||
|
self.header_button.mouseMoveEvent = self._header_mouse_move_event
|
||||||
|
self.header_button.dragEnterEvent = self._header_drag_enter_event
|
||||||
|
self.header_button.dropEvent = self._header_drop_event
|
||||||
|
|
||||||
|
self.drag_start_position = None
|
||||||
|
|
||||||
|
# Add header to layout
|
||||||
|
header_layout.addWidget(self.header_button)
|
||||||
|
header_layout.addStretch()
|
||||||
|
|
||||||
|
self.header_add_button = QPushButton()
|
||||||
|
self.header_add_button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
|
self.header_add_button.setFixedSize(20, 20)
|
||||||
|
self.header_add_button.setToolTip("Add item")
|
||||||
|
self.header_add_button.setVisible(show_add_button)
|
||||||
|
|
||||||
|
self.header_add_button.setIcon(material_icon("add", size=(20, 20)))
|
||||||
|
header_layout.addWidget(self.header_add_button)
|
||||||
|
|
||||||
|
self.main_layout.addLayout(header_layout)
|
||||||
|
|
||||||
|
self._update_expanded_state()
|
||||||
|
|
||||||
|
def set_widget(self, widget):
|
||||||
|
"""Set the content widget for this collapsible section"""
|
||||||
|
# Remove existing content widget if any
|
||||||
|
if self.content_widget and self.content_widget.parent() == self:
|
||||||
|
self.main_layout.removeWidget(self.content_widget)
|
||||||
|
self.content_widget.close()
|
||||||
|
self.content_widget.deleteLater()
|
||||||
|
|
||||||
|
self.content_widget = widget
|
||||||
|
if self.content_widget:
|
||||||
|
self.main_layout.addWidget(self.content_widget)
|
||||||
|
|
||||||
|
self._update_expanded_state()
|
||||||
|
|
||||||
|
def _update_appearance(self):
|
||||||
|
"""Update the header button appearance based on expanded state"""
|
||||||
|
# Use material icons with consistent sizing to match tree items
|
||||||
|
icon_name = "keyboard_arrow_down" if self.expanded else "keyboard_arrow_right"
|
||||||
|
icon = material_icon(icon_name=icon_name, size=(20, 20), convert_to_pixmap=False)
|
||||||
|
|
||||||
|
self.header_button.setIcon(icon)
|
||||||
|
self.header_button.setText(self.title)
|
||||||
|
|
||||||
|
# Get theme colors
|
||||||
|
palette = get_theme_palette()
|
||||||
|
text_color = palette.text().color().name()
|
||||||
|
|
||||||
|
self.header_button.setStyleSheet(
|
||||||
|
f"""
|
||||||
|
QPushButton {{
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: {text_color};
|
||||||
|
icon-size: 20px 20px;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def toggle_expanded(self):
|
||||||
|
"""Toggle the expanded state and update size policy"""
|
||||||
|
self.expanded = not self.expanded
|
||||||
|
self._update_expanded_state()
|
||||||
|
|
||||||
|
def _update_expanded_state(self):
|
||||||
|
"""Update the expanded state based on current state"""
|
||||||
|
self._update_appearance()
|
||||||
|
if self.expanded:
|
||||||
|
if self.content_widget:
|
||||||
|
self.content_widget.show()
|
||||||
|
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
|
else:
|
||||||
|
if self.content_widget:
|
||||||
|
self.content_widget.hide()
|
||||||
|
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
|
|
||||||
|
@SafeProperty(bool)
|
||||||
|
def expanded(self) -> bool:
|
||||||
|
"""Get the expanded state"""
|
||||||
|
return self._expanded
|
||||||
|
|
||||||
|
@expanded.setter
|
||||||
|
def expanded(self, value: bool):
|
||||||
|
"""Set the expanded state programmatically"""
|
||||||
|
if not isinstance(value, bool):
|
||||||
|
raise ValueError("Expanded state must be a boolean")
|
||||||
|
if self._expanded == value:
|
||||||
|
return
|
||||||
|
self._expanded = value
|
||||||
|
self._update_appearance()
|
||||||
|
|
||||||
|
def connect_add_button(self, slot):
|
||||||
|
"""Connect a slot to the add button's clicked signal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slot: The function to call when the add button is clicked.
|
||||||
|
"""
|
||||||
|
self.header_add_button.clicked.connect(slot)
|
||||||
|
|
||||||
|
def _header_mouse_press_event(self, event):
|
||||||
|
"""Handle mouse press on header for drag start"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.drag_start_position = event.pos()
|
||||||
|
QPushButton.mousePressEvent(self.header_button, event)
|
||||||
|
|
||||||
|
def _header_mouse_move_event(self, event):
|
||||||
|
"""Handle mouse move to start drag operation"""
|
||||||
|
if event.buttons() & Qt.MouseButton.LeftButton and self.drag_start_position is not None:
|
||||||
|
|
||||||
|
# Check if we've moved far enough to start a drag
|
||||||
|
if (event.pos() - self.drag_start_position).manhattanLength() >= 10:
|
||||||
|
|
||||||
|
self._start_drag()
|
||||||
|
QPushButton.mouseMoveEvent(self.header_button, event)
|
||||||
|
|
||||||
|
def _start_drag(self):
|
||||||
|
"""Start the drag operation with a properly aligned widget pixmap"""
|
||||||
|
drag = QDrag(self.header_button)
|
||||||
|
mime_data = QMimeData()
|
||||||
|
mime_data.setText(f"section:{self.title}")
|
||||||
|
drag.setMimeData(mime_data)
|
||||||
|
|
||||||
|
# Grab a pixmap of the widget
|
||||||
|
widget_pixmap = self.header_button.grab()
|
||||||
|
|
||||||
|
drag.setPixmap(widget_pixmap)
|
||||||
|
|
||||||
|
# Set the hotspot to where the mouse was pressed on the widget
|
||||||
|
drag.setHotSpot(self.drag_start_position)
|
||||||
|
|
||||||
|
drag.exec_(Qt.MoveAction)
|
||||||
|
|
||||||
|
def _header_drag_enter_event(self, event):
|
||||||
|
"""Handle drag enter on header"""
|
||||||
|
if event.mimeData().hasText() and event.mimeData().text().startswith("section:"):
|
||||||
|
event.acceptProposedAction()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
def _header_drop_event(self, event):
|
||||||
|
"""Handle drop on header"""
|
||||||
|
if event.mimeData().hasText() and event.mimeData().text().startswith("section:"):
|
||||||
|
source_title = event.mimeData().text().replace("section:", "")
|
||||||
|
if source_title != self.title:
|
||||||
|
# Emit signal to parent to handle reordering
|
||||||
|
self.section_reorder_requested.emit(source_title, self.title)
|
||||||
|
event.acceptProposedAction()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from qtpy.QtCore import Qt
|
||||||
|
from qtpy.QtWidgets import QSizePolicy, QSpacerItem, QSplitter, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.colors import get_theme_palette
|
||||||
|
from bec_widgets.widgets.containers.explorer.collapsible_tree_section import CollapsibleSection
|
||||||
|
|
||||||
|
|
||||||
|
class Explorer(BECWidget, QWidget):
|
||||||
|
"""
|
||||||
|
A widget that combines multiple collapsible sections for an explorer-like interface.
|
||||||
|
Each section can be expanded or collapsed, and sections can be reordered. The explorer
|
||||||
|
can contain also sub-explorers for nested structures.
|
||||||
|
"""
|
||||||
|
|
||||||
|
RPC = False
|
||||||
|
PLUGIN = False
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# Main layout
|
||||||
|
self.main_layout = QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.main_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Splitter for sections
|
||||||
|
self.splitter = QSplitter(Qt.Orientation.Vertical)
|
||||||
|
self.main_layout.addWidget(self.splitter)
|
||||||
|
|
||||||
|
# Spacer for when all sections are collapsed
|
||||||
|
self.expander = QSpacerItem(0, 0)
|
||||||
|
self.main_layout.addItem(self.expander)
|
||||||
|
|
||||||
|
# Registry of sections
|
||||||
|
self.sections: list[CollapsibleSection] = []
|
||||||
|
|
||||||
|
# Setup splitter styling
|
||||||
|
self._setup_splitter_styling()
|
||||||
|
|
||||||
|
def add_section(self, section: CollapsibleSection) -> None:
|
||||||
|
"""
|
||||||
|
Add a collapsible section to the explorer
|
||||||
|
|
||||||
|
Args:
|
||||||
|
section (CollapsibleSection): The section to add
|
||||||
|
"""
|
||||||
|
if not isinstance(section, CollapsibleSection):
|
||||||
|
raise TypeError("section must be an instance of CollapsibleSection")
|
||||||
|
|
||||||
|
if section in self.sections:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.sections.append(section)
|
||||||
|
self.splitter.addWidget(section)
|
||||||
|
|
||||||
|
# Connect the section's toggle to update spacer
|
||||||
|
section.header_button.clicked.connect(self._update_spacer)
|
||||||
|
|
||||||
|
# Connect section reordering if supported
|
||||||
|
if hasattr(section, "section_reorder_requested"):
|
||||||
|
section.section_reorder_requested.connect(self._handle_section_reorder)
|
||||||
|
|
||||||
|
self._update_spacer()
|
||||||
|
|
||||||
|
def remove_section(self, section: CollapsibleSection) -> None:
|
||||||
|
"""
|
||||||
|
Remove a collapsible section from the explorer
|
||||||
|
|
||||||
|
Args:
|
||||||
|
section (CollapsibleSection): The section to remove
|
||||||
|
"""
|
||||||
|
if section not in self.sections:
|
||||||
|
return
|
||||||
|
self.sections.remove(section)
|
||||||
|
section.deleteLater()
|
||||||
|
section.close()
|
||||||
|
|
||||||
|
# Disconnect signals
|
||||||
|
try:
|
||||||
|
section.header_button.clicked.disconnect(self._update_spacer)
|
||||||
|
if hasattr(section, "section_reorder_requested"):
|
||||||
|
section.section_reorder_requested.disconnect(self._handle_section_reorder)
|
||||||
|
except RuntimeError:
|
||||||
|
# Signals already disconnected
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._update_spacer()
|
||||||
|
|
||||||
|
def get_section(self, title: str) -> CollapsibleSection | None:
|
||||||
|
"""Get a section by its title"""
|
||||||
|
for section in self.sections:
|
||||||
|
if section.title == title:
|
||||||
|
return section
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _setup_splitter_styling(self) -> None:
|
||||||
|
"""Setup the splitter styling with theme colors"""
|
||||||
|
palette = get_theme_palette()
|
||||||
|
separator_color = palette.mid().color()
|
||||||
|
|
||||||
|
self.splitter.setStyleSheet(
|
||||||
|
f"""
|
||||||
|
QSplitter::handle {{
|
||||||
|
height: 0.1px;
|
||||||
|
background-color: rgba({separator_color.red()}, {separator_color.green()}, {separator_color.blue()}, 60);
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_spacer(self) -> None:
|
||||||
|
"""Update the spacer size based on section states"""
|
||||||
|
any_expanded = any(section.expanded for section in self.sections)
|
||||||
|
|
||||||
|
if any_expanded:
|
||||||
|
self.expander.changeSize(0, 0, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
|
else:
|
||||||
|
self.expander.changeSize(
|
||||||
|
0, 10, QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Expanding
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_section_reorder(self, source_title: str, target_title: str) -> None:
|
||||||
|
"""Handle reordering of sections"""
|
||||||
|
if source_title == target_title:
|
||||||
|
return
|
||||||
|
|
||||||
|
source_section = self.get_section(source_title)
|
||||||
|
target_section = self.get_section(target_title)
|
||||||
|
|
||||||
|
if not source_section or not target_section:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current indices
|
||||||
|
source_index = self.splitter.indexOf(source_section)
|
||||||
|
target_index = self.splitter.indexOf(target_section)
|
||||||
|
|
||||||
|
if source_index == -1 or target_index == -1:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Insert at target position
|
||||||
|
self.splitter.insertWidget(target_index, source_section)
|
||||||
|
|
||||||
|
# Update sections
|
||||||
|
self.sections.remove(source_section)
|
||||||
|
self.sections.insert(target_index, source_section)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import os
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication, QLabel
|
||||||
|
|
||||||
|
from bec_widgets.widgets.containers.explorer.script_tree_widget import ScriptTreeWidget
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
explorer = Explorer()
|
||||||
|
section = CollapsibleSection(title="SCRIPTS", indentation=0)
|
||||||
|
|
||||||
|
script_explorer = Explorer()
|
||||||
|
script_widget = ScriptTreeWidget()
|
||||||
|
local_scripts_section = CollapsibleSection(title="Local")
|
||||||
|
local_scripts_section.set_widget(script_widget)
|
||||||
|
script_widget.set_directory(os.path.abspath("./"))
|
||||||
|
script_explorer.add_section(local_scripts_section)
|
||||||
|
|
||||||
|
section.set_widget(script_explorer)
|
||||||
|
explorer.add_section(section)
|
||||||
|
shared_script_section = CollapsibleSection(title="Shared")
|
||||||
|
shared_script_widget = ScriptTreeWidget()
|
||||||
|
shared_script_widget.set_directory(os.path.abspath("./"))
|
||||||
|
shared_script_section.set_widget(shared_script_widget)
|
||||||
|
script_explorer.add_section(shared_script_section)
|
||||||
|
macros_section = CollapsibleSection(title="MACROS", indentation=0)
|
||||||
|
macros_section.set_widget(QLabel("Macros will be implemented later"))
|
||||||
|
explorer.add_section(macros_section)
|
||||||
|
explorer.show()
|
||||||
|
app.exec()
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from qtpy.QtCore import QModelIndex, QRect, QRegularExpression, QSortFilterProxyModel, Qt, Signal
|
||||||
|
from qtpy.QtGui import QAction, QPainter
|
||||||
|
from qtpy.QtWidgets import QFileSystemModel, QStyledItemDelegate, QTreeView, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.colors import get_theme_palette
|
||||||
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class FileItemDelegate(QStyledItemDelegate):
|
||||||
|
"""Custom delegate to show action buttons on hover"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.hovered_index = QModelIndex()
|
||||||
|
self.file_actions: list[QAction] = []
|
||||||
|
self.dir_actions: list[QAction] = []
|
||||||
|
self.button_rects: list[QRect] = []
|
||||||
|
self.current_file_path = ""
|
||||||
|
|
||||||
|
def add_file_action(self, action: QAction) -> None:
|
||||||
|
"""Add an action for files"""
|
||||||
|
self.file_actions.append(action)
|
||||||
|
|
||||||
|
def add_dir_action(self, action: QAction) -> None:
|
||||||
|
"""Add an action for directories"""
|
||||||
|
self.dir_actions.append(action)
|
||||||
|
|
||||||
|
def clear_actions(self) -> None:
|
||||||
|
"""Remove all actions"""
|
||||||
|
self.file_actions.clear()
|
||||||
|
self.dir_actions.clear()
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
"""Paint the item with action buttons on hover"""
|
||||||
|
# Paint the default item
|
||||||
|
super().paint(painter, option, index)
|
||||||
|
|
||||||
|
# Early return if not hovering over this item
|
||||||
|
if index != self.hovered_index:
|
||||||
|
return
|
||||||
|
|
||||||
|
tree_view = self.parent()
|
||||||
|
if not isinstance(tree_view, QTreeView):
|
||||||
|
return
|
||||||
|
|
||||||
|
proxy_model = tree_view.model()
|
||||||
|
if not isinstance(proxy_model, QSortFilterProxyModel):
|
||||||
|
return
|
||||||
|
|
||||||
|
source_index = proxy_model.mapToSource(index)
|
||||||
|
source_model = proxy_model.sourceModel()
|
||||||
|
if not isinstance(source_model, QFileSystemModel):
|
||||||
|
return
|
||||||
|
|
||||||
|
is_dir = source_model.isDir(source_index)
|
||||||
|
file_path = source_model.filePath(source_index)
|
||||||
|
self.current_file_path = file_path
|
||||||
|
|
||||||
|
# Choose appropriate actions based on item type
|
||||||
|
actions = self.dir_actions if is_dir else self.file_actions
|
||||||
|
if actions:
|
||||||
|
self._draw_action_buttons(painter, option, actions)
|
||||||
|
|
||||||
|
def _draw_action_buttons(self, painter, option, actions: list[QAction]):
|
||||||
|
"""Draw action buttons on the right side"""
|
||||||
|
button_size = 18
|
||||||
|
margin = 4
|
||||||
|
spacing = 2
|
||||||
|
|
||||||
|
# Calculate total width needed for all buttons
|
||||||
|
total_width = len(actions) * button_size + (len(actions) - 1) * spacing
|
||||||
|
|
||||||
|
# Clear previous button rects and create new ones
|
||||||
|
self.button_rects.clear()
|
||||||
|
|
||||||
|
# Calculate starting position (right side of the item)
|
||||||
|
start_x = option.rect.right() - total_width - margin
|
||||||
|
current_x = start_x
|
||||||
|
|
||||||
|
painter.save()
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
|
||||||
|
# Get theme colors for better integration
|
||||||
|
palette = get_theme_palette()
|
||||||
|
button_bg = palette.button().color()
|
||||||
|
button_bg.setAlpha(150) # Semi-transparent
|
||||||
|
|
||||||
|
for action in actions:
|
||||||
|
if not action.isVisible():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate button position
|
||||||
|
button_rect = QRect(
|
||||||
|
current_x,
|
||||||
|
option.rect.top() + (option.rect.height() - button_size) // 2,
|
||||||
|
button_size,
|
||||||
|
button_size,
|
||||||
|
)
|
||||||
|
self.button_rects.append(button_rect)
|
||||||
|
|
||||||
|
# Draw button background
|
||||||
|
painter.setBrush(button_bg)
|
||||||
|
painter.setPen(palette.mid().color())
|
||||||
|
painter.drawRoundedRect(button_rect, 3, 3)
|
||||||
|
|
||||||
|
# Draw action icon
|
||||||
|
icon = action.icon()
|
||||||
|
if not icon.isNull():
|
||||||
|
icon_rect = button_rect.adjusted(2, 2, -2, -2)
|
||||||
|
icon.paint(painter, icon_rect)
|
||||||
|
|
||||||
|
# Move to next button position
|
||||||
|
current_x += button_size + spacing
|
||||||
|
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
def editorEvent(self, event, model, option, index):
|
||||||
|
"""Handle mouse events for action buttons"""
|
||||||
|
# Early return if not a left click
|
||||||
|
if not (
|
||||||
|
event.type() == event.Type.MouseButtonPress
|
||||||
|
and event.button() == Qt.MouseButton.LeftButton
|
||||||
|
):
|
||||||
|
return super().editorEvent(event, model, option, index)
|
||||||
|
|
||||||
|
# Early return if not a proxy model
|
||||||
|
if not isinstance(model, QSortFilterProxyModel):
|
||||||
|
return super().editorEvent(event, model, option, index)
|
||||||
|
|
||||||
|
source_index = model.mapToSource(index)
|
||||||
|
source_model = model.sourceModel()
|
||||||
|
|
||||||
|
# Early return if not a file system model
|
||||||
|
if not isinstance(source_model, QFileSystemModel):
|
||||||
|
return super().editorEvent(event, model, option, index)
|
||||||
|
|
||||||
|
is_dir = source_model.isDir(source_index)
|
||||||
|
actions = self.dir_actions if is_dir else self.file_actions
|
||||||
|
|
||||||
|
# Check which button was clicked
|
||||||
|
visible_actions = [action for action in actions if action.isVisible()]
|
||||||
|
for i, button_rect in enumerate(self.button_rects):
|
||||||
|
if button_rect.contains(event.pos()) and i < len(visible_actions):
|
||||||
|
# Trigger the action
|
||||||
|
visible_actions[i].trigger()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return super().editorEvent(event, model, option, index)
|
||||||
|
|
||||||
|
def set_hovered_index(self, index):
|
||||||
|
"""Set the currently hovered index"""
|
||||||
|
self.hovered_index = index
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptTreeWidget(QWidget):
|
||||||
|
"""A simple tree widget for scripts using QFileSystemModel - designed to be injected into CollapsibleSection"""
|
||||||
|
|
||||||
|
file_selected = Signal(str) # Script file path selected
|
||||||
|
file_open_requested = Signal(str) # File open button clicked
|
||||||
|
file_renamed = Signal(str, str) # Old path, new path
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# Create layout
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Create tree view
|
||||||
|
self.tree = QTreeView()
|
||||||
|
self.tree.setHeaderHidden(True)
|
||||||
|
self.tree.setRootIsDecorated(True)
|
||||||
|
|
||||||
|
# Enable mouse tracking for hover effects
|
||||||
|
self.tree.setMouseTracking(True)
|
||||||
|
|
||||||
|
# Create file system model
|
||||||
|
self.model = QFileSystemModel()
|
||||||
|
self.model.setNameFilters(["*.py"])
|
||||||
|
self.model.setNameFilterDisables(False)
|
||||||
|
|
||||||
|
# Create proxy model to filter out underscore directories
|
||||||
|
self.proxy_model = QSortFilterProxyModel()
|
||||||
|
self.proxy_model.setFilterRegularExpression(QRegularExpression("^[^_].*"))
|
||||||
|
self.proxy_model.setSourceModel(self.model)
|
||||||
|
self.tree.setModel(self.proxy_model)
|
||||||
|
|
||||||
|
# Create and set custom delegate
|
||||||
|
self.delegate = FileItemDelegate(self.tree)
|
||||||
|
self.tree.setItemDelegate(self.delegate)
|
||||||
|
|
||||||
|
# Add default open button for files
|
||||||
|
action = MaterialIconAction(icon_name="file_open", tooltip="Open file", parent=self)
|
||||||
|
action.action.triggered.connect(self._on_file_open_requested)
|
||||||
|
self.delegate.add_file_action(action.action)
|
||||||
|
|
||||||
|
# Remove unnecessary columns
|
||||||
|
self.tree.setColumnHidden(1, True) # Hide size column
|
||||||
|
self.tree.setColumnHidden(2, True) # Hide type column
|
||||||
|
self.tree.setColumnHidden(3, True) # Hide date modified column
|
||||||
|
|
||||||
|
# Apply BEC styling
|
||||||
|
self._apply_styling()
|
||||||
|
|
||||||
|
# Script specific properties
|
||||||
|
self.directory = None
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
self.tree.clicked.connect(self._on_item_clicked)
|
||||||
|
self.tree.doubleClicked.connect(self._on_item_double_clicked)
|
||||||
|
|
||||||
|
# Install event filter for hover tracking
|
||||||
|
self.tree.viewport().installEventFilter(self)
|
||||||
|
|
||||||
|
# Add to layout
|
||||||
|
layout.addWidget(self.tree)
|
||||||
|
|
||||||
|
def _apply_styling(self):
|
||||||
|
"""Apply styling to the tree widget"""
|
||||||
|
# Get theme colors for subtle tree lines
|
||||||
|
palette = get_theme_palette()
|
||||||
|
subtle_line_color = palette.mid().color()
|
||||||
|
subtle_line_color.setAlpha(80)
|
||||||
|
|
||||||
|
# pylint: disable=f-string-without-interpolation
|
||||||
|
tree_style = f"""
|
||||||
|
QTreeView {{
|
||||||
|
border: none;
|
||||||
|
outline: 0;
|
||||||
|
show-decoration-selected: 0;
|
||||||
|
}}
|
||||||
|
QTreeView::branch {{
|
||||||
|
border-image: none;
|
||||||
|
background: transparent;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTreeView::item {{
|
||||||
|
border: none;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}}
|
||||||
|
QTreeView::item:hover {{
|
||||||
|
background: palette(midlight);
|
||||||
|
border: none;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
text-decoration: none;
|
||||||
|
}}
|
||||||
|
QTreeView::item:selected {{
|
||||||
|
background: palette(highlight);
|
||||||
|
color: palette(highlighted-text);
|
||||||
|
}}
|
||||||
|
QTreeView::item:selected:hover {{
|
||||||
|
background: palette(highlight);
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.tree.setStyleSheet(tree_style)
|
||||||
|
|
||||||
|
def eventFilter(self, obj, event):
|
||||||
|
"""Handle mouse move events for hover tracking"""
|
||||||
|
# Early return if not the tree viewport
|
||||||
|
if obj != self.tree.viewport():
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
if event.type() == event.Type.MouseMove:
|
||||||
|
index = self.tree.indexAt(event.pos())
|
||||||
|
if index.isValid():
|
||||||
|
self.delegate.set_hovered_index(index)
|
||||||
|
else:
|
||||||
|
self.delegate.set_hovered_index(QModelIndex())
|
||||||
|
self.tree.viewport().update()
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
if event.type() == event.Type.Leave:
|
||||||
|
self.delegate.set_hovered_index(QModelIndex())
|
||||||
|
self.tree.viewport().update()
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
def set_directory(self, directory):
|
||||||
|
"""Set the scripts directory"""
|
||||||
|
self.directory = directory
|
||||||
|
|
||||||
|
# Early return if directory doesn't exist
|
||||||
|
if not directory or not os.path.exists(directory):
|
||||||
|
return
|
||||||
|
|
||||||
|
root_index = self.model.setRootPath(directory)
|
||||||
|
# Map the source model index to proxy model index
|
||||||
|
proxy_root_index = self.proxy_model.mapFromSource(root_index)
|
||||||
|
self.tree.setRootIndex(proxy_root_index)
|
||||||
|
self.tree.expandAll()
|
||||||
|
|
||||||
|
def _on_item_clicked(self, index: QModelIndex):
|
||||||
|
"""Handle item clicks"""
|
||||||
|
# Map proxy index back to source index
|
||||||
|
source_index = self.proxy_model.mapToSource(index)
|
||||||
|
|
||||||
|
# Early return for directories
|
||||||
|
if self.model.isDir(source_index):
|
||||||
|
return
|
||||||
|
|
||||||
|
file_path = self.model.filePath(source_index)
|
||||||
|
|
||||||
|
# Early return if not a valid file
|
||||||
|
if not file_path or not os.path.isfile(file_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
path_obj = Path(file_path)
|
||||||
|
|
||||||
|
# Only emit signal for Python files
|
||||||
|
if path_obj.suffix.lower() == ".py":
|
||||||
|
logger.info(f"Script selected: {file_path}")
|
||||||
|
self.file_selected.emit(file_path)
|
||||||
|
|
||||||
|
def _on_item_double_clicked(self, index: QModelIndex):
|
||||||
|
"""Handle item double-clicks"""
|
||||||
|
# Map proxy index back to source index
|
||||||
|
source_index = self.proxy_model.mapToSource(index)
|
||||||
|
|
||||||
|
# Early return for directories
|
||||||
|
if self.model.isDir(source_index):
|
||||||
|
return
|
||||||
|
|
||||||
|
file_path = self.model.filePath(source_index)
|
||||||
|
|
||||||
|
# Early return if not a valid file
|
||||||
|
if not file_path or not os.path.isfile(file_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Emit signal to open the file
|
||||||
|
logger.info(f"File open requested via double-click: {file_path}")
|
||||||
|
self.file_open_requested.emit(file_path)
|
||||||
|
|
||||||
|
def _on_file_open_requested(self):
|
||||||
|
"""Handle file open action triggered"""
|
||||||
|
logger.info("File open requested")
|
||||||
|
# Early return if no hovered item
|
||||||
|
if not self.delegate.hovered_index.isValid():
|
||||||
|
return
|
||||||
|
|
||||||
|
source_index = self.proxy_model.mapToSource(self.delegate.hovered_index)
|
||||||
|
file_path = self.model.filePath(source_index)
|
||||||
|
|
||||||
|
# Early return if not a valid file
|
||||||
|
if not file_path or not os.path.isfile(file_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.file_open_requested.emit(file_path)
|
||||||
|
|
||||||
|
def add_file_action(self, action: QAction) -> None:
|
||||||
|
"""Add an action for file items"""
|
||||||
|
self.delegate.add_file_action(action)
|
||||||
|
|
||||||
|
def add_dir_action(self, action: QAction) -> None:
|
||||||
|
"""Add an action for directory items"""
|
||||||
|
self.delegate.add_dir_action(action)
|
||||||
|
|
||||||
|
def clear_actions(self) -> None:
|
||||||
|
"""Remove all actions from items"""
|
||||||
|
self.delegate.clear_actions()
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""Refresh the tree view"""
|
||||||
|
if self.directory is None:
|
||||||
|
return
|
||||||
|
self.model.setRootPath("") # Reset
|
||||||
|
root_index = self.model.setRootPath(self.directory)
|
||||||
|
proxy_root_index = self.proxy_model.mapFromSource(root_index)
|
||||||
|
self.tree.setRootIndex(proxy_root_index)
|
||||||
|
|
||||||
|
def expand_all(self):
|
||||||
|
"""Expand all items in the tree"""
|
||||||
|
self.tree.expandAll()
|
||||||
|
|
||||||
|
def collapse_all(self):
|
||||||
|
"""Collapse all items in the tree"""
|
||||||
|
self.tree.collapseAll()
|
||||||
@@ -12,4 +12,4 @@ class BECWebLinksMixin:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def open_bec_bug_report():
|
def open_bec_bug_report():
|
||||||
webbrowser.open("https://gitlab.psi.ch/groups/bec/-/issues/")
|
webbrowser.open("https://github.com/bec-project/bec_widgets/issues")
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{'files': ['main_window.py']}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
from qtpy.QtCore import QSize
|
||||||
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtGui import QIcon
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
|
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||||
|
|
||||||
|
DOM_XML = """
|
||||||
|
<ui language='c++'>
|
||||||
|
<widget class='BECMainWindow' name='bec_main_window'>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BECMainWindowPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._form_editor = None
|
||||||
|
|
||||||
|
def createWidget(self, parent):
|
||||||
|
# We want to initialize BECMainWindow upon starting designer
|
||||||
|
t = BECMainWindow(parent)
|
||||||
|
return t
|
||||||
|
|
||||||
|
def domXml(self):
|
||||||
|
return DOM_XML
|
||||||
|
|
||||||
|
def group(self):
|
||||||
|
return "BEC Containers"
|
||||||
|
|
||||||
|
def icon(self):
|
||||||
|
return designer_material_icon(BECMainWindow.ICON_NAME)
|
||||||
|
|
||||||
|
def includeFile(self):
|
||||||
|
return "bec_main_window"
|
||||||
|
|
||||||
|
def initialize(self, form_editor):
|
||||||
|
import os
|
||||||
|
|
||||||
|
from qtpy.QtCore import Qt
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
import bec_widgets
|
||||||
|
|
||||||
|
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||||
|
QApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)
|
||||||
|
app = QApplication.instance()
|
||||||
|
icon = QIcon()
|
||||||
|
icon.addFile(
|
||||||
|
os.path.join(MODULE_PATH, "assets", "app_icons", "BEC-General-App.png"),
|
||||||
|
size=QSize(48, 48),
|
||||||
|
)
|
||||||
|
app.setWindowIcon(icon)
|
||||||
|
self._form_editor = form_editor
|
||||||
|
|
||||||
|
def isContainer(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def isInitialized(self):
|
||||||
|
return self._form_editor is not None
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "BECMainWindow"
|
||||||
|
|
||||||
|
def toolTip(self):
|
||||||
|
return "BECMainWindow"
|
||||||
|
|
||||||
|
def whatsThis(self):
|
||||||
|
return self.toolTip()
|
||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from qtpy.QtCore import QEasingCurve, QEvent, QPropertyAnimation, QSize, Qt, QTimer
|
from qtpy.QtCore import QEvent, QSize, Qt, QTimer
|
||||||
from qtpy.QtGui import QAction, QActionGroup, QIcon
|
from qtpy.QtGui import QAction, QActionGroup, QIcon
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
@@ -34,12 +34,15 @@ from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanP
|
|||||||
|
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||||
|
|
||||||
|
# Ensure the application does not use the native menu bar on macOS to be consistent with linux development.
|
||||||
|
QApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)
|
||||||
|
|
||||||
|
|
||||||
class BECMainWindow(BECWidget, QMainWindow):
|
class BECMainWindow(BECWidget, QMainWindow):
|
||||||
RPC = False
|
RPC = True
|
||||||
PLUGIN = False
|
PLUGIN = True
|
||||||
SCAN_PROGRESS_WIDTH = 100 # px
|
SCAN_PROGRESS_WIDTH = 100 # px
|
||||||
STATUS_BAR_WIDGETS_EXPIRE_TIME = 60_000 # milliseconds
|
SCAN_PROGRESS_HEIGHT = 12 # px
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -198,8 +201,8 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
self._scan_progress_bar_simple.show_remaining_time = False
|
self._scan_progress_bar_simple.show_remaining_time = False
|
||||||
self._scan_progress_bar_simple.show_source_label = False
|
self._scan_progress_bar_simple.show_source_label = False
|
||||||
self._scan_progress_bar_simple.progressbar.label_template = ""
|
self._scan_progress_bar_simple.progressbar.label_template = ""
|
||||||
self._scan_progress_bar_simple.progressbar.setFixedHeight(8)
|
self._scan_progress_bar_simple.progressbar.setFixedHeight(self.SCAN_PROGRESS_HEIGHT)
|
||||||
self._scan_progress_bar_simple.progressbar.setFixedWidth(80)
|
self._scan_progress_bar_simple.progressbar.setFixedWidth(self.SCAN_PROGRESS_WIDTH)
|
||||||
self._scan_progress_bar_full = ScanProgressBar(self)
|
self._scan_progress_bar_full = ScanProgressBar(self)
|
||||||
self._scan_progress_hover = HoverWidget(
|
self._scan_progress_hover = HoverWidget(
|
||||||
self, simple=self._scan_progress_bar_simple, full=self._scan_progress_bar_full
|
self, simple=self._scan_progress_bar_simple, full=self._scan_progress_bar_full
|
||||||
@@ -216,62 +219,8 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
self._scan_progress_bar_with_separator.layout.addWidget(separator)
|
self._scan_progress_bar_with_separator.layout.addWidget(separator)
|
||||||
self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_hover)
|
self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_hover)
|
||||||
|
|
||||||
# Set Size
|
|
||||||
self._scan_progress_bar_target_width = self.SCAN_PROGRESS_WIDTH
|
|
||||||
self._scan_progress_bar_with_separator.setMaximumWidth(self._scan_progress_bar_target_width)
|
|
||||||
|
|
||||||
self.status_bar.addWidget(self._scan_progress_bar_with_separator)
|
self.status_bar.addWidget(self._scan_progress_bar_with_separator)
|
||||||
|
|
||||||
# Visibility logic
|
|
||||||
self._scan_progress_bar_with_separator.hide()
|
|
||||||
self._scan_progress_bar_with_separator.setMaximumWidth(0)
|
|
||||||
|
|
||||||
# Timer for hiding logic
|
|
||||||
self._scan_progress_hide_timer = QTimer(self)
|
|
||||||
self._scan_progress_hide_timer.setSingleShot(True)
|
|
||||||
self._scan_progress_hide_timer.setInterval(self.STATUS_BAR_WIDGETS_EXPIRE_TIME)
|
|
||||||
self._scan_progress_hide_timer.timeout.connect(self._animate_hide_scan_progress_bar)
|
|
||||||
|
|
||||||
# Show / hide behaviour
|
|
||||||
self._scan_progress_bar_simple.progress_started.connect(self._show_scan_progress_bar)
|
|
||||||
self._scan_progress_bar_simple.progress_finished.connect(self._delay_hide_scan_progress_bar)
|
|
||||||
|
|
||||||
def _show_scan_progress_bar(self):
|
|
||||||
if self._scan_progress_hide_timer.isActive():
|
|
||||||
self._scan_progress_hide_timer.stop()
|
|
||||||
if self._scan_progress_bar_with_separator.isVisible():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Make visible and reset width
|
|
||||||
self._scan_progress_bar_with_separator.show()
|
|
||||||
self._scan_progress_bar_with_separator.setMaximumWidth(0)
|
|
||||||
|
|
||||||
self._show_container_anim = QPropertyAnimation(
|
|
||||||
self._scan_progress_bar_with_separator, b"maximumWidth", self
|
|
||||||
)
|
|
||||||
self._show_container_anim.setDuration(300)
|
|
||||||
self._show_container_anim.setStartValue(0)
|
|
||||||
self._show_container_anim.setEndValue(self._scan_progress_bar_target_width)
|
|
||||||
self._show_container_anim.setEasingCurve(QEasingCurve.OutCubic)
|
|
||||||
self._show_container_anim.start()
|
|
||||||
|
|
||||||
def _delay_hide_scan_progress_bar(self):
|
|
||||||
"""Start the countdown to hide the scan progress bar."""
|
|
||||||
if hasattr(self, "_scan_progress_hide_timer"):
|
|
||||||
self._scan_progress_hide_timer.start()
|
|
||||||
|
|
||||||
def _animate_hide_scan_progress_bar(self):
|
|
||||||
"""Shrink container to the right, then hide."""
|
|
||||||
self._hide_container_anim = QPropertyAnimation(
|
|
||||||
self._scan_progress_bar_with_separator, b"maximumWidth", self
|
|
||||||
)
|
|
||||||
self._hide_container_anim.setDuration(300)
|
|
||||||
self._hide_container_anim.setStartValue(self._scan_progress_bar_with_separator.width())
|
|
||||||
self._hide_container_anim.setEndValue(0)
|
|
||||||
self._hide_container_anim.setEasingCurve(QEasingCurve.InCubic)
|
|
||||||
self._hide_container_anim.finished.connect(self._scan_progress_bar_with_separator.hide)
|
|
||||||
self._hide_container_anim.start()
|
|
||||||
|
|
||||||
def _add_separator(self, separate_object: bool = False) -> QWidget | None:
|
def _add_separator(self, separate_object: bool = False) -> QWidget | None:
|
||||||
"""
|
"""
|
||||||
Add a vertically centred separator to the status bar or just return it as a separate object.
|
Add a vertically centred separator to the status bar or just return it as a separate object.
|
||||||
@@ -471,8 +420,6 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
# Timer cleanup
|
# Timer cleanup
|
||||||
if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
|
if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
|
||||||
self._client_info_expire_timer.stop()
|
self._client_info_expire_timer.stop()
|
||||||
if hasattr(self, "_scan_progress_hide_timer") and self._scan_progress_hide_timer.isActive():
|
|
||||||
self._scan_progress_hide_timer.stop()
|
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
# Status bar widgets cleanup
|
# Status bar widgets cleanup
|
||||||
@@ -491,15 +438,16 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
class UILaunchWindow(BECMainWindow):
|
class BECMainWindowNoRPC(BECMainWindow):
|
||||||
RPC = True
|
RPC = False
|
||||||
|
PLUGIN = False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
main_window = UILaunchWindow()
|
main_window = BECMainWindow()
|
||||||
main_window.show()
|
main_window.show()
|
||||||
main_window.resize(800, 600)
|
main_window.resize(800, 600)
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
def main(): # pragma: no cover
|
||||||
|
from qtpy import PYSIDE6
|
||||||
|
|
||||||
|
if not PYSIDE6:
|
||||||
|
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||||
|
return
|
||||||
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
|
from bec_widgets.widgets.containers.main_window.bec_main_window_plugin import (
|
||||||
|
BECMainWindowPlugin,
|
||||||
|
)
|
||||||
|
|
||||||
|
QPyDesignerCustomWidgetCollection.addCustomWidget(BECMainWindowPlugin())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
|
from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
|
||||||
@@ -20,6 +21,8 @@ class AbortButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = AbortButton(parent)
|
t = AbortButton(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.buttons.button_reset.button_reset import ResetButton
|
from bec_widgets.widgets.control.buttons.button_reset.button_reset import ResetButton
|
||||||
@@ -20,6 +21,8 @@ class ResetButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = ResetButton(parent)
|
t = ResetButton(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ class ResetButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "ResetButton"
|
return "ResetButton"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "A button that reset the scan queue."
|
return "A button that resets the scan queue."
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.buttons.button_resume.button_resume import ResumeButton
|
from bec_widgets.widgets.control.buttons.button_resume.button_resume import ResumeButton
|
||||||
@@ -20,6 +21,8 @@ class ResumeButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = ResumeButton(parent)
|
t = ResumeButton(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
import bec_widgets
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
|
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
|
||||||
|
|
||||||
@@ -15,8 +14,6 @@ DOM_XML = """
|
|||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -24,6 +21,8 @@ class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = StopButton(parent)
|
t = StopButton(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -1,10 +1,9 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
import bec_widgets
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import (
|
from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import (
|
||||||
PositionIndicator,
|
PositionIndicator,
|
||||||
@@ -17,8 +16,6 @@ DOM_XML = """
|
|||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -26,6 +23,8 @@ class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = PositionIndicator(parent)
|
t = PositionIndicator(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
|
|||||||
return "PositionIndicator"
|
return "PositionIndicator"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "PositionIndicator"
|
return ""
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
+17
-8
@@ -88,7 +88,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
|||||||
if not self._check_device_is_valid(device):
|
if not self._check_device_is_valid(device):
|
||||||
return
|
return
|
||||||
|
|
||||||
data = self.dev[device].read()
|
data = self.dev[device].read(cached=True)
|
||||||
self._on_device_readback(
|
self._on_device_readback(
|
||||||
device,
|
device,
|
||||||
self._device_ui_components(device),
|
self._device_ui_components(device),
|
||||||
@@ -138,7 +138,11 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
|||||||
signals = msg_content.get("signals", {})
|
signals = msg_content.get("signals", {})
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
hinted_signals = self.dev[device]._hints
|
hinted_signals = self.dev[device]._hints
|
||||||
precision = self.dev[device].precision
|
precision = getattr(self.dev[device], "precision", 8)
|
||||||
|
try:
|
||||||
|
precision = int(precision)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
precision = int(8)
|
||||||
|
|
||||||
spinner = ui_components["spinner"]
|
spinner = ui_components["spinner"]
|
||||||
position_indicator = ui_components["position_indicator"]
|
position_indicator = ui_components["position_indicator"]
|
||||||
@@ -178,11 +182,13 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
|||||||
spinner.setVisible(False)
|
spinner.setVisible(False)
|
||||||
|
|
||||||
if readback_val is not None:
|
if readback_val is not None:
|
||||||
readback.setText(f"{readback_val:.{precision}f}")
|
text = f"{readback_val:.{precision}f}"
|
||||||
|
readback.setText(text)
|
||||||
position_emit(readback_val)
|
position_emit(readback_val)
|
||||||
|
|
||||||
if setpoint_val is not None:
|
if setpoint_val is not None:
|
||||||
setpoint.setText(f"{setpoint_val:.{precision}f}")
|
text = f"{setpoint_val:.{precision}f}"
|
||||||
|
setpoint.setText(text)
|
||||||
|
|
||||||
limits = self.dev[device].limits
|
limits = self.dev[device].limits
|
||||||
limit_update(limits)
|
limit_update(limits)
|
||||||
@@ -205,10 +211,13 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
|||||||
ui["readback"].setToolTip(f"{device} readback")
|
ui["readback"].setToolTip(f"{device} readback")
|
||||||
ui["setpoint"].setToolTip(f"{device} setpoint")
|
ui["setpoint"].setToolTip(f"{device} setpoint")
|
||||||
ui["step_size"].setToolTip(f"Step size for {device}")
|
ui["step_size"].setToolTip(f"Step size for {device}")
|
||||||
precision = self.dev[device].precision
|
precision = getattr(self.dev[device], "precision", 8)
|
||||||
if precision is not None:
|
try:
|
||||||
ui["step_size"].setDecimals(precision)
|
precision = int(precision)
|
||||||
ui["step_size"].setValue(10**-precision * 10)
|
except (TypeError, ValueError):
|
||||||
|
precision = int(8)
|
||||||
|
ui["step_size"].setDecimals(precision)
|
||||||
|
ui["step_size"].setValue(10**-precision * 10)
|
||||||
|
|
||||||
def _swap_readback_signal_connection(self, slot, old_device, new_device):
|
def _swap_readback_signal_connection(self, slot, old_device, new_device):
|
||||||
self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))
|
self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ class PositionerBox(PositionerBoxBase):
|
|||||||
PLUGIN = True
|
PLUGIN = True
|
||||||
RPC = True
|
RPC = True
|
||||||
|
|
||||||
USER_ACCESS = ["set_positioner"]
|
USER_ACCESS = ["set_positioner", "screenshot"]
|
||||||
device_changed = Signal(str, str)
|
device_changed = Signal(str, str)
|
||||||
# Signal emitted to inform listeners about a position update
|
# Signal emitted to inform listeners about a position update
|
||||||
position_update = Signal(float)
|
position_update = Signal(float)
|
||||||
|
|||||||
+7
-5
@@ -1,12 +1,13 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
|
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box import (
|
||||||
|
PositionerBox,
|
||||||
|
)
|
||||||
|
|
||||||
DOM_XML = """
|
DOM_XML = """
|
||||||
<ui language='c++'>
|
<ui language='c++'>
|
||||||
@@ -14,7 +15,6 @@ DOM_XML = """
|
|||||||
</widget>
|
</widget>
|
||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
|
|
||||||
|
|
||||||
class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
@@ -23,6 +23,8 @@ class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = PositionerBox(parent)
|
t = PositionerBox(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "Device Control"
|
return "BEC Device Control"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(PositionerBox.ICON_NAME)
|
return designer_material_icon(PositionerBox.ICON_NAME)
|
||||||
|
|||||||
+4
-1
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
|
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
|
||||||
@@ -22,6 +23,8 @@ class PositionerBox2DPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = PositionerBox2D(parent)
|
t = PositionerBox2D(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -29,7 +32,7 @@ class PositionerBox2DPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "Device Control"
|
return "BEC Device Control"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(PositionerBox2D.ICON_NAME)
|
return designer_material_icon(PositionerBox2D.ICON_NAME)
|
||||||
|
|||||||
+52
-1
@@ -34,7 +34,15 @@ class PositionerBox2D(PositionerBoxBase):
|
|||||||
|
|
||||||
PLUGIN = True
|
PLUGIN = True
|
||||||
RPC = True
|
RPC = True
|
||||||
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver"]
|
USER_ACCESS = [
|
||||||
|
"set_positioner_hor",
|
||||||
|
"set_positioner_ver",
|
||||||
|
"screenshot",
|
||||||
|
"enable_controls_hor",
|
||||||
|
"enable_controls_hor.setter",
|
||||||
|
"enable_controls_ver",
|
||||||
|
"enable_controls_ver.setter",
|
||||||
|
]
|
||||||
|
|
||||||
device_changed_hor = Signal(str, str)
|
device_changed_hor = Signal(str, str)
|
||||||
device_changed_ver = Signal(str, str)
|
device_changed_ver = Signal(str, str)
|
||||||
@@ -63,6 +71,8 @@ class PositionerBox2D(PositionerBoxBase):
|
|||||||
self._limits_hor = None
|
self._limits_hor = None
|
||||||
self._limits_ver = None
|
self._limits_ver = None
|
||||||
self._dialog = None
|
self._dialog = None
|
||||||
|
self._enable_controls_hor = True
|
||||||
|
self._enable_controls_ver = True
|
||||||
if self.current_path == "":
|
if self.current_path == "":
|
||||||
self.current_path = os.path.dirname(__file__)
|
self.current_path = os.path.dirname(__file__)
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
@@ -281,6 +291,7 @@ class PositionerBox2D(PositionerBoxBase):
|
|||||||
self.on_device_readback_hor,
|
self.on_device_readback_hor,
|
||||||
self._device_ui_components_hv("horizontal"),
|
self._device_ui_components_hv("horizontal"),
|
||||||
)
|
)
|
||||||
|
self._apply_controls_enabled("horizontal")
|
||||||
|
|
||||||
@SafeSlot(str, str)
|
@SafeSlot(str, str)
|
||||||
def on_device_change_ver(self, old_device: str, new_device: str):
|
def on_device_change_ver(self, old_device: str, new_device: str):
|
||||||
@@ -300,6 +311,7 @@ class PositionerBox2D(PositionerBoxBase):
|
|||||||
self.on_device_readback_ver,
|
self.on_device_readback_ver,
|
||||||
self._device_ui_components_hv("vertical"),
|
self._device_ui_components_hv("vertical"),
|
||||||
)
|
)
|
||||||
|
self._apply_controls_enabled("vertical")
|
||||||
|
|
||||||
def _device_ui_components_hv(self, device: DeviceId) -> DeviceUpdateUIComponents:
|
def _device_ui_components_hv(self, device: DeviceId) -> DeviceUpdateUIComponents:
|
||||||
if device == "horizontal":
|
if device == "horizontal":
|
||||||
@@ -337,6 +349,25 @@ class PositionerBox2D(PositionerBoxBase):
|
|||||||
if device == self.device_ver:
|
if device == self.device_ver:
|
||||||
return self._device_ui_components_hv("vertical")
|
return self._device_ui_components_hv("vertical")
|
||||||
|
|
||||||
|
def _apply_controls_enabled(self, axis: DeviceId):
|
||||||
|
state = self._enable_controls_hor if axis == "horizontal" else self._enable_controls_ver
|
||||||
|
if axis == "horizontal":
|
||||||
|
widgets = [
|
||||||
|
self.ui.tweak_increase_hor,
|
||||||
|
self.ui.tweak_decrease_hor,
|
||||||
|
self.ui.step_increase_hor,
|
||||||
|
self.ui.step_decrease_hor,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
widgets = [
|
||||||
|
self.ui.tweak_increase_ver,
|
||||||
|
self.ui.tweak_decrease_ver,
|
||||||
|
self.ui.step_increase_ver,
|
||||||
|
self.ui.step_decrease_ver,
|
||||||
|
]
|
||||||
|
for w in widgets:
|
||||||
|
w.setEnabled(state)
|
||||||
|
|
||||||
@SafeSlot(dict, dict)
|
@SafeSlot(dict, dict)
|
||||||
def on_device_readback_hor(self, msg_content: dict, metadata: dict):
|
def on_device_readback_hor(self, msg_content: dict, metadata: dict):
|
||||||
"""Callback for device readback.
|
"""Callback for device readback.
|
||||||
@@ -417,6 +448,26 @@ class PositionerBox2D(PositionerBoxBase):
|
|||||||
"""Step size for tweak"""
|
"""Step size for tweak"""
|
||||||
self.ui.step_size_ver.setValue(val)
|
self.ui.step_size_ver.setValue(val)
|
||||||
|
|
||||||
|
@SafeProperty(bool)
|
||||||
|
def enable_controls_hor(self) -> bool:
|
||||||
|
"""Persisted switch for horizontal control buttons (tweak/step)."""
|
||||||
|
return self._enable_controls_hor
|
||||||
|
|
||||||
|
@enable_controls_hor.setter
|
||||||
|
def enable_controls_hor(self, value: bool):
|
||||||
|
self._enable_controls_hor = value
|
||||||
|
self._apply_controls_enabled("horizontal")
|
||||||
|
|
||||||
|
@SafeProperty(bool)
|
||||||
|
def enable_controls_ver(self) -> bool:
|
||||||
|
"""Persisted switch for vertical control buttons (tweak/step)."""
|
||||||
|
return self._enable_controls_ver
|
||||||
|
|
||||||
|
@enable_controls_ver.setter
|
||||||
|
def enable_controls_ver(self, value: bool):
|
||||||
|
self._enable_controls_ver = value
|
||||||
|
self._apply_controls_enabled("vertical")
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def on_tweak_inc_hor(self):
|
def on_tweak_inc_hor(self):
|
||||||
"""Tweak device a up"""
|
"""Tweak device a up"""
|
||||||
|
|||||||
+8
-6
@@ -1,12 +1,13 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerControlLine
|
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line import (
|
||||||
|
PositionerControlLine,
|
||||||
|
)
|
||||||
|
|
||||||
DOM_XML = """
|
DOM_XML = """
|
||||||
<ui language='c++'>
|
<ui language='c++'>
|
||||||
@@ -14,7 +15,6 @@ DOM_XML = """
|
|||||||
</widget>
|
</widget>
|
||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
|
|
||||||
|
|
||||||
class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
@@ -23,6 +23,8 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = PositionerControlLine(parent)
|
t = PositionerControlLine(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "Device Control"
|
return "BEC Device Control"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(PositionerControlLine.ICON_NAME)
|
return designer_material_icon(PositionerControlLine.ICON_NAME)
|
||||||
@@ -51,7 +53,7 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
|
|||||||
return "PositionerControlLine"
|
return "PositionerControlLine"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "A widget that controls a single positioner in line form."
|
return "A widget that controls a single device."
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ logger = bec_logger.logger
|
|||||||
|
|
||||||
|
|
||||||
class PositionerGroupBox(QGroupBox):
|
class PositionerGroupBox(QGroupBox):
|
||||||
PLUGIN = True
|
|
||||||
position_update = Signal(float)
|
position_update = Signal(float)
|
||||||
|
|
||||||
def __init__(self, parent, dev_name):
|
def __init__(self, parent, dev_name):
|
||||||
@@ -45,7 +45,12 @@ class PositionerGroupBox(QGroupBox):
|
|||||||
|
|
||||||
def _on_position_update(self, pos: float):
|
def _on_position_update(self, pos: float):
|
||||||
self.position_update.emit(pos)
|
self.position_update.emit(pos)
|
||||||
self.widget.label = f"%.{self.widget.dev[self.widget.device].precision}f" % pos
|
precision = getattr(self.widget.dev[self.widget.device], "precision", 8)
|
||||||
|
try:
|
||||||
|
precision = int(precision)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
precision = int(8)
|
||||||
|
self.widget.label = f"{pos:.{precision}f}"
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.widget.close()
|
self.widget.close()
|
||||||
@@ -55,6 +60,7 @@ class PositionerGroupBox(QGroupBox):
|
|||||||
class PositionerGroup(BECWidget, QWidget):
|
class PositionerGroup(BECWidget, QWidget):
|
||||||
"""Simple Widget to control a positioner in box form"""
|
"""Simple Widget to control a positioner in box form"""
|
||||||
|
|
||||||
|
PLUGIN = True
|
||||||
ICON_NAME = "grid_view"
|
ICON_NAME = "grid_view"
|
||||||
USER_ACCESS = ["set_positioners"]
|
USER_ACCESS = ["set_positioners"]
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{'files': ['positioner_group.py']}
|
{'files': ['positioner_group.py']}
|
||||||
+5
-5
@@ -1,9 +1,8 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_control.positioner_group.positioner_group import (
|
from bec_widgets.widgets.control.device_control.positioner_group.positioner_group import (
|
||||||
@@ -16,7 +15,6 @@ DOM_XML = """
|
|||||||
</widget>
|
</widget>
|
||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
|
|
||||||
|
|
||||||
class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
@@ -25,6 +23,8 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = PositionerGroup(parent)
|
t = PositionerGroup(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "Device Control"
|
return "BEC Device Control"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(PositionerGroup.ICON_NAME)
|
return designer_material_icon(PositionerGroup.ICON_NAME)
|
||||||
@@ -53,7 +53,7 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "PositionerGroup"
|
return "PositionerGroup"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "Container Widget to control positioners in compact form, in a grid"
|
return "Simple Widget to control a positioner in box form"
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ class DeviceInputBase(BECWidget):
|
|||||||
WidgetIO.set_value(widget=self, value=device)
|
WidgetIO.set_value(widget=self, value=device)
|
||||||
self.config.default = device
|
self.config.default = device
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Device {device} is not in the filtered selection.")
|
logger.warning(
|
||||||
|
f"Device {device} is not in the filtered selection of {self}: {self.devices}."
|
||||||
|
)
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def update_devices_from_filters(self):
|
def update_devices_from_filters(self):
|
||||||
@@ -131,7 +133,8 @@ class DeviceInputBase(BECWidget):
|
|||||||
# Filter based on readout priority
|
# Filter based on readout priority
|
||||||
devs = [dev for dev in devs if self._check_readout_filter(dev)]
|
devs = [dev for dev in devs if self._check_readout_filter(dev)]
|
||||||
self.devices = [device.name for device in devs]
|
self.devices = [device.name for device in devs]
|
||||||
self.set_device(current_device)
|
if current_device != "":
|
||||||
|
self.set_device(current_device)
|
||||||
|
|
||||||
@SafeSlot(list)
|
@SafeSlot(list)
|
||||||
def set_available_devices(self, devices: list[str]):
|
def set_available_devices(self, devices: list[str]):
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
{
|
{'files': ['device_combobox.py']}
|
||||||
"files": ["device_combobox.py"]
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,19 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
import bec_widgets
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||||
|
|
||||||
DOM_XML = """
|
DOM_XML = """
|
||||||
<ui language='c++'>
|
<ui language='c++'>
|
||||||
<widget class='DeviceComboBox' name='device_combobox'>
|
<widget class='DeviceComboBox' name='device_combo_box'>
|
||||||
</widget>
|
</widget>
|
||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -24,6 +21,8 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = DeviceComboBox(parent)
|
t = DeviceComboBox(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return designer_material_icon(DeviceComboBox.ICON_NAME)
|
return designer_material_icon(DeviceComboBox.ICON_NAME)
|
||||||
|
|
||||||
def includeFile(self):
|
def includeFile(self):
|
||||||
return "device_combobox"
|
return "device_combo_box"
|
||||||
|
|
||||||
def initialize(self, form_editor):
|
def initialize(self, form_editor):
|
||||||
self._form_editor = form_editor
|
self._form_editor = form_editor
|
||||||
@@ -52,7 +51,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "DeviceComboBox"
|
return "DeviceComboBox"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "Device ComboBox Example for BEC Widgets"
|
return ""
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
+1
-3
@@ -1,3 +1 @@
|
|||||||
{
|
{'files': ['device_line_edit.py']}
|
||||||
"files": ["device_line_edit.py"]
|
|
||||||
}
|
|
||||||
+4
-5
@@ -1,10 +1,9 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
import bec_widgets
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||||
DeviceLineEdit,
|
DeviceLineEdit,
|
||||||
@@ -17,8 +16,6 @@ DOM_XML = """
|
|||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -26,6 +23,8 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = DeviceLineEdit(parent)
|
t = DeviceLineEdit(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "DeviceLineEdit"
|
return "DeviceLineEdit"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "Device LineEdit Example for BEC Widgets with autocomplete."
|
return ""
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
||||||
@@ -20,6 +21,8 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = SignalComboBox(parent)
|
t = SignalComboBox(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "SignalComboBox"
|
return "SignalComboBox"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "Signal ComboBox Example for BEC Widgets with autocomplete."
|
return ""
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
+4
-1
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.device_input.signal_line_edit.signal_line_edit import (
|
from bec_widgets.widgets.control.device_input.signal_line_edit.signal_line_edit import (
|
||||||
@@ -22,6 +23,8 @@ class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = SignalLineEdit(parent)
|
t = SignalLineEdit(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -50,7 +53,7 @@ class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "SignalLineEdit"
|
return "SignalLineEdit"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "Signal LineEdit Example for BEC Widgets with autocomplete."
|
return ""
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -0,0 +1,631 @@
|
|||||||
|
"""Module with the device table view implementation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_qthemes import material_icon
|
||||||
|
from qtpy import QtCore, QtGui, QtWidgets
|
||||||
|
from thefuzz import fuzz
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.colors import get_accent_colors
|
||||||
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
# Threshold for fuzzy matching, careful with adjusting this. 80 seems good
|
||||||
|
FUZZY_SEARCH_THRESHOLD = 80
|
||||||
|
|
||||||
|
|
||||||
|
class DictToolTipDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
|
"""Delegate that shows all key-value pairs of a rows's data as a YAML-like tooltip."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dict_to_str(d: dict) -> str:
|
||||||
|
"""Convert a dictionary to a formatted string."""
|
||||||
|
return json.dumps(d, indent=4)
|
||||||
|
|
||||||
|
def helpEvent(self, event, view, option, index):
|
||||||
|
"""Override to show tooltip when hovering."""
|
||||||
|
if event.type() != QtCore.QEvent.ToolTip:
|
||||||
|
return super().helpEvent(event, view, option, index)
|
||||||
|
model: DeviceFilterProxyModel = index.model()
|
||||||
|
model_index = model.mapToSource(index)
|
||||||
|
row_dict = model.sourceModel().row_data(model_index)
|
||||||
|
row_dict.pop("description", None)
|
||||||
|
QtWidgets.QToolTip.showText(event.globalPos(), self.dict_to_str(row_dict), view)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class CenterCheckBoxDelegate(DictToolTipDelegate):
|
||||||
|
"""Custom checkbox delegate to center checkboxes in table cells."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
colors = get_accent_colors()
|
||||||
|
self._icon_checked = material_icon(
|
||||||
|
"check_box", size=QtCore.QSize(16, 16), color=colors.default
|
||||||
|
)
|
||||||
|
self._icon_unchecked = material_icon(
|
||||||
|
"check_box_outline_blank", size=QtCore.QSize(16, 16), color=colors.default
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_theme(self, theme: str | None = None):
|
||||||
|
colors = get_accent_colors()
|
||||||
|
self._icon_checked.setColor(colors.default)
|
||||||
|
self._icon_unchecked.setColor(colors.default)
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
value = index.model().data(index, QtCore.Qt.CheckStateRole)
|
||||||
|
if value is None:
|
||||||
|
super().paint(painter, option, index)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Choose icon based on state
|
||||||
|
pixmap = self._icon_checked if value == QtCore.Qt.Checked else self._icon_unchecked
|
||||||
|
|
||||||
|
# Draw icon centered
|
||||||
|
rect = option.rect
|
||||||
|
pix_rect = pixmap.rect()
|
||||||
|
pix_rect.moveCenter(rect.center())
|
||||||
|
painter.drawPixmap(pix_rect.topLeft(), pixmap)
|
||||||
|
|
||||||
|
def editorEvent(self, event, model, option, index):
|
||||||
|
if event.type() != QtCore.QEvent.MouseButtonRelease:
|
||||||
|
return False
|
||||||
|
current = model.data(index, QtCore.Qt.CheckStateRole)
|
||||||
|
new_state = QtCore.Qt.Unchecked if current == QtCore.Qt.Checked else QtCore.Qt.Checked
|
||||||
|
return model.setData(index, new_state, QtCore.Qt.CheckStateRole)
|
||||||
|
|
||||||
|
|
||||||
|
class WrappingTextDelegate(DictToolTipDelegate):
|
||||||
|
"""Custom delegate for wrapping text in table cells."""
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
text = index.model().data(index, QtCore.Qt.DisplayRole)
|
||||||
|
if not text:
|
||||||
|
return super().paint(painter, option, index)
|
||||||
|
|
||||||
|
painter.save()
|
||||||
|
painter.setClipRect(option.rect)
|
||||||
|
text_option = QtCore.Qt.TextWordWrap | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
|
||||||
|
painter.drawText(option.rect.adjusted(4, 2, -4, -2), text_option, text)
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
def sizeHint(self, option, index):
|
||||||
|
text = str(index.model().data(index, QtCore.Qt.DisplayRole) or "")
|
||||||
|
# if not text:
|
||||||
|
# return super().sizeHint(option, index)
|
||||||
|
|
||||||
|
# Use the actual column width
|
||||||
|
table = index.model().parent() # or store reference to QTableView
|
||||||
|
column_width = table.columnWidth(index.column()) # - 8
|
||||||
|
|
||||||
|
doc = QtGui.QTextDocument()
|
||||||
|
doc.setDefaultFont(option.font)
|
||||||
|
doc.setTextWidth(column_width)
|
||||||
|
doc.setPlainText(text)
|
||||||
|
|
||||||
|
layout_height = doc.documentLayout().documentSize().height()
|
||||||
|
height = int(layout_height) + 4 # Needs some extra padding, otherwise it gets cut off
|
||||||
|
return QtCore.QSize(column_width, height)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTableModel(QtCore.QAbstractTableModel):
|
||||||
|
"""
|
||||||
|
Custom Device Table Model for managing device configurations.
|
||||||
|
|
||||||
|
Sort logic is implemented directly on the data of the table view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, device_config: list[dict] | None = None, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._device_config = device_config or []
|
||||||
|
self.headers = [
|
||||||
|
"name",
|
||||||
|
"deviceClass",
|
||||||
|
"readoutPriority",
|
||||||
|
"enabled",
|
||||||
|
"readOnly",
|
||||||
|
"deviceTags",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
self._checkable_columns_enabled = {"enabled": True, "readOnly": True}
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
########## Overwrite custom Qt methods ########
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
def rowCount(self, parent=QtCore.QModelIndex()) -> int:
|
||||||
|
return len(self._device_config)
|
||||||
|
|
||||||
|
def columnCount(self, parent=QtCore.QModelIndex()) -> int:
|
||||||
|
return len(self.headers)
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
|
||||||
|
if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal:
|
||||||
|
return self.headers[section]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def row_data(self, index: QtCore.QModelIndex) -> dict:
|
||||||
|
"""Return the row data for the given index."""
|
||||||
|
if not index.isValid():
|
||||||
|
return {}
|
||||||
|
return copy.deepcopy(self._device_config[index.row()])
|
||||||
|
|
||||||
|
def data(self, index, role=QtCore.Qt.DisplayRole):
|
||||||
|
"""Return data for the given index and role."""
|
||||||
|
if not index.isValid():
|
||||||
|
return None
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
key = self.headers[col]
|
||||||
|
value = self._device_config[row].get(key)
|
||||||
|
|
||||||
|
if role == QtCore.Qt.DisplayRole:
|
||||||
|
if key in ("enabled", "readOnly"):
|
||||||
|
return bool(value)
|
||||||
|
if key == "deviceTags":
|
||||||
|
return ", ".join(str(tag) for tag in value) if value else ""
|
||||||
|
return str(value) if value is not None else ""
|
||||||
|
if role == QtCore.Qt.CheckStateRole and key in ("enabled", "readOnly"):
|
||||||
|
return QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
|
||||||
|
if role == QtCore.Qt.TextAlignmentRole:
|
||||||
|
if key in ("enabled", "readOnly"):
|
||||||
|
return QtCore.Qt.AlignCenter
|
||||||
|
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
|
||||||
|
if role == QtCore.Qt.FontRole:
|
||||||
|
font = QtGui.QFont()
|
||||||
|
return font
|
||||||
|
return None
|
||||||
|
|
||||||
|
def flags(self, index):
|
||||||
|
"""Flags for the table model."""
|
||||||
|
if not index.isValid():
|
||||||
|
return QtCore.Qt.NoItemFlags
|
||||||
|
key = self.headers[index.column()]
|
||||||
|
|
||||||
|
if key in ("enabled", "readOnly"):
|
||||||
|
base_flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||||
|
if self._checkable_columns_enabled.get(key, True):
|
||||||
|
return base_flags | QtCore.Qt.ItemIsUserCheckable
|
||||||
|
else:
|
||||||
|
return base_flags # disable editing but still visible
|
||||||
|
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||||
|
|
||||||
|
def setData(self, index, value, role=QtCore.Qt.EditRole) -> bool:
|
||||||
|
"""
|
||||||
|
Method to set the data of the table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index (QModelIndex): The index of the item to modify.
|
||||||
|
value (Any): The new value to set.
|
||||||
|
role (Qt.ItemDataRole): The role of the data being set.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the data was set successfully, False otherwise.
|
||||||
|
"""
|
||||||
|
if not index.isValid():
|
||||||
|
return False
|
||||||
|
key = self.headers[index.column()]
|
||||||
|
row = index.row()
|
||||||
|
|
||||||
|
if key in ("enabled", "readOnly") and role == QtCore.Qt.CheckStateRole:
|
||||||
|
if not self._checkable_columns_enabled.get(key, True):
|
||||||
|
return False # ignore changes if column is disabled
|
||||||
|
self._device_config[row][key] = value == QtCore.Qt.Checked
|
||||||
|
self.dataChanged.emit(index, index, [QtCore.Qt.CheckStateRole])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
####################################
|
||||||
|
############ Public methods ########
|
||||||
|
####################################
|
||||||
|
|
||||||
|
def get_device_config(self) -> list[dict]:
|
||||||
|
"""Return the current device config (with checkbox updates applied)."""
|
||||||
|
return self._device_config
|
||||||
|
|
||||||
|
def set_checkbox_enabled(self, column_name: str, enabled: bool):
|
||||||
|
"""
|
||||||
|
Enable/Disable the checkbox column.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
column_name (str): The name of the column to modify.
|
||||||
|
enabled (bool): Whether the checkbox should be enabled or disabled.
|
||||||
|
"""
|
||||||
|
if column_name in self._checkable_columns_enabled:
|
||||||
|
self._checkable_columns_enabled[column_name] = enabled
|
||||||
|
col = self.headers.index(column_name)
|
||||||
|
top_left = self.index(0, col)
|
||||||
|
bottom_right = self.index(self.rowCount() - 1, col)
|
||||||
|
self.dataChanged.emit(
|
||||||
|
top_left, bottom_right, [QtCore.Qt.CheckStateRole, QtCore.Qt.DisplayRole]
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_device_config(self, device_config: list[dict]):
|
||||||
|
"""
|
||||||
|
Replace the device config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_config (list[dict]): The new device config to set.
|
||||||
|
"""
|
||||||
|
self.beginResetModel()
|
||||||
|
self._device_config = list(device_config)
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
@SafeSlot(dict)
|
||||||
|
def add_device(self, device: dict):
|
||||||
|
"""
|
||||||
|
Add an extra device to the device config at the bottom.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device (dict): The device configuration to add.
|
||||||
|
"""
|
||||||
|
row = len(self._device_config)
|
||||||
|
self.beginInsertRows(QtCore.QModelIndex(), row, row)
|
||||||
|
self._device_config.append(device)
|
||||||
|
self.endInsertRows()
|
||||||
|
|
||||||
|
@SafeSlot(int)
|
||||||
|
def remove_device_by_row(self, row: int):
|
||||||
|
"""
|
||||||
|
Remove one device row by index. This maps to the row to the source of the data model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row (int): The index of the device row to remove.
|
||||||
|
"""
|
||||||
|
if 0 <= row < len(self._device_config):
|
||||||
|
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
|
||||||
|
self._device_config.pop(row)
|
||||||
|
self.endRemoveRows()
|
||||||
|
|
||||||
|
@SafeSlot(list)
|
||||||
|
def remove_devices_by_rows(self, rows: list[int]):
|
||||||
|
"""
|
||||||
|
Remove multiple device rows by their indices.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rows (list[int]): The indices of the device rows to remove.
|
||||||
|
"""
|
||||||
|
for row in sorted(rows, reverse=True):
|
||||||
|
self.remove_device_by_row(row)
|
||||||
|
|
||||||
|
@SafeSlot(str)
|
||||||
|
def remove_device_by_name(self, name: str):
|
||||||
|
"""
|
||||||
|
Remove one device row by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the device to remove.
|
||||||
|
"""
|
||||||
|
for row, device in enumerate(self._device_config):
|
||||||
|
if device.get("name") == name:
|
||||||
|
self.remove_device_by_row(row)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class BECTableView(QtWidgets.QTableView):
|
||||||
|
"""Table View with custom keyPressEvent to delete rows with backspace or delete key"""
|
||||||
|
|
||||||
|
def keyPressEvent(self, event) -> None:
|
||||||
|
"""
|
||||||
|
Delete selected rows with backspace or delete key
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: keyPressEvent
|
||||||
|
"""
|
||||||
|
if event.key() not in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
|
||||||
|
return super().keyPressEvent(event)
|
||||||
|
|
||||||
|
proxy_indexes = self.selectedIndexes()
|
||||||
|
if not proxy_indexes:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get unique rows (proxy indices) in reverse order so removal indexes stay valid
|
||||||
|
proxy_rows = sorted({idx.row() for idx in proxy_indexes}, reverse=True)
|
||||||
|
# Map to source model rows
|
||||||
|
source_rows = [
|
||||||
|
self.model().mapToSource(self.model().index(row, 0)).row() for row in proxy_rows
|
||||||
|
]
|
||||||
|
|
||||||
|
model: DeviceTableModel = self.model().sourceModel() # access underlying model
|
||||||
|
# Delegate confirmation and removal to helper
|
||||||
|
removed = self._confirm_and_remove_rows(model, source_rows)
|
||||||
|
if not removed:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _confirm_and_remove_rows(self, model: DeviceTableModel, source_rows: list[int]) -> bool:
|
||||||
|
"""
|
||||||
|
Prompt the user to confirm removal of rows and remove them from the model if accepted.
|
||||||
|
|
||||||
|
Returns True if rows were removed, False otherwise.
|
||||||
|
"""
|
||||||
|
cfg = model.get_device_config()
|
||||||
|
names = [str(cfg[r].get("name", "<unknown>")) for r in sorted(source_rows)]
|
||||||
|
|
||||||
|
msg = QtWidgets.QMessageBox(self)
|
||||||
|
msg.setIcon(QtWidgets.QMessageBox.Warning)
|
||||||
|
msg.setWindowTitle("Confirm remove devices")
|
||||||
|
if len(names) == 1:
|
||||||
|
msg.setText(f"Remove device '{names[0]}'?")
|
||||||
|
else:
|
||||||
|
msg.setText(f"Remove {len(names)} devices?")
|
||||||
|
msg.setInformativeText("\n".join(names))
|
||||||
|
msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
|
||||||
|
msg.setDefaultButton(QtWidgets.QMessageBox.Cancel)
|
||||||
|
|
||||||
|
res = msg.exec_()
|
||||||
|
if res == QtWidgets.QMessageBox.Ok:
|
||||||
|
model.remove_devices_by_rows(source_rows)
|
||||||
|
# TODO add signal for removed devices
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._hidden_rows = set()
|
||||||
|
self._filter_text = ""
|
||||||
|
self._enable_fuzzy = True
|
||||||
|
self._filter_columns = [0, 1] # name and deviceClass for search
|
||||||
|
|
||||||
|
def hide_rows(self, row_indices: list[int]):
|
||||||
|
"""
|
||||||
|
Hide specific rows in the model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row_indices (list[int]): List of row indices to hide.
|
||||||
|
"""
|
||||||
|
self._hidden_rows.update(row_indices)
|
||||||
|
self.invalidateFilter()
|
||||||
|
|
||||||
|
def show_rows(self, row_indices: list[int]):
|
||||||
|
"""
|
||||||
|
Show specific rows in the model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row_indices (list[int]): List of row indices to show.
|
||||||
|
"""
|
||||||
|
self._hidden_rows.difference_update(row_indices)
|
||||||
|
self.invalidateFilter()
|
||||||
|
|
||||||
|
def show_all_rows(self):
|
||||||
|
"""
|
||||||
|
Show all rows in the model.
|
||||||
|
"""
|
||||||
|
self._hidden_rows.clear()
|
||||||
|
self.invalidateFilter()
|
||||||
|
|
||||||
|
@SafeSlot(int)
|
||||||
|
def disable_fuzzy_search(self, enabled: int):
|
||||||
|
self._enable_fuzzy = not bool(enabled)
|
||||||
|
self.invalidateFilter()
|
||||||
|
|
||||||
|
def setFilterText(self, text: str):
|
||||||
|
self._filter_text = text.lower()
|
||||||
|
self.invalidateFilter()
|
||||||
|
|
||||||
|
def filterAcceptsRow(self, source_row: int, source_parent) -> bool:
|
||||||
|
# No hidden rows, and no filter text
|
||||||
|
if not self._filter_text and not self._hidden_rows:
|
||||||
|
return True
|
||||||
|
# Hide hidden rows
|
||||||
|
if source_row in self._hidden_rows:
|
||||||
|
return False
|
||||||
|
# Check the filter text for each row
|
||||||
|
model = self.sourceModel()
|
||||||
|
text = self._filter_text.lower()
|
||||||
|
for column in self._filter_columns:
|
||||||
|
index = model.index(source_row, column, source_parent)
|
||||||
|
data = str(model.data(index, QtCore.Qt.DisplayRole) or "")
|
||||||
|
if self._enable_fuzzy is True:
|
||||||
|
match_ratio = fuzz.partial_ratio(self._filter_text.lower(), data.lower())
|
||||||
|
if match_ratio >= FUZZY_SEARCH_THRESHOLD:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if text in data.lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTableView(BECWidget, QtWidgets.QWidget):
|
||||||
|
"""Device Table View for the device manager."""
|
||||||
|
|
||||||
|
RPC = False
|
||||||
|
PLUGIN = False
|
||||||
|
devices_removed = QtCore.Signal(list)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, client=None):
|
||||||
|
super().__init__(client=client, parent=parent, theme_update=True)
|
||||||
|
|
||||||
|
self.layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Setup table view
|
||||||
|
self._setup_table_view()
|
||||||
|
# Setup search view, needs table proxy to be iniditate
|
||||||
|
self._setup_search()
|
||||||
|
# Add widgets to main layout
|
||||||
|
self.layout.addLayout(self.search_controls)
|
||||||
|
self.layout.addWidget(self.table)
|
||||||
|
|
||||||
|
def _setup_search(self):
|
||||||
|
"""Create components related to the search functionality"""
|
||||||
|
|
||||||
|
# Create search bar
|
||||||
|
self.search_layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.search_label = QtWidgets.QLabel("Search:")
|
||||||
|
self.search_input = QtWidgets.QLineEdit()
|
||||||
|
self.search_input.setPlaceholderText(
|
||||||
|
"Filter devices (approximate matching)..."
|
||||||
|
) # Default to fuzzy search
|
||||||
|
self.search_input.setClearButtonEnabled(True)
|
||||||
|
self.search_input.textChanged.connect(self.proxy.setFilterText)
|
||||||
|
self.search_layout.addWidget(self.search_label)
|
||||||
|
self.search_layout.addWidget(self.search_input)
|
||||||
|
|
||||||
|
# Add exact match toggle
|
||||||
|
self.fuzzy_layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.fuzzy_label = QtWidgets.QLabel("Exact Match:")
|
||||||
|
self.fuzzy_is_disabled = QtWidgets.QCheckBox()
|
||||||
|
|
||||||
|
self.fuzzy_is_disabled.stateChanged.connect(self.proxy.disable_fuzzy_search)
|
||||||
|
self.fuzzy_is_disabled.setToolTip(
|
||||||
|
"Enable approximate matching (OFF) and exact matching (ON)"
|
||||||
|
)
|
||||||
|
self.fuzzy_label.setToolTip("Enable approximate matching (OFF) and exact matching (ON)")
|
||||||
|
self.fuzzy_layout.addWidget(self.fuzzy_label)
|
||||||
|
self.fuzzy_layout.addWidget(self.fuzzy_is_disabled)
|
||||||
|
self.fuzzy_layout.addStretch()
|
||||||
|
|
||||||
|
# Add both search components to the layout
|
||||||
|
self.search_controls = QtWidgets.QHBoxLayout()
|
||||||
|
self.search_controls.addLayout(self.search_layout)
|
||||||
|
self.search_controls.addSpacing(20) # Add some space between the search box and toggle
|
||||||
|
self.search_controls.addLayout(self.fuzzy_layout)
|
||||||
|
QtCore.QTimer.singleShot(0, lambda: self.fuzzy_is_disabled.stateChanged.emit(0))
|
||||||
|
|
||||||
|
def _setup_table_view(self) -> None:
|
||||||
|
"""Setup the table view."""
|
||||||
|
# Model + Proxy
|
||||||
|
self.table = BECTableView(self)
|
||||||
|
self.model = DeviceTableModel(parent=self.table)
|
||||||
|
self.proxy = DeviceFilterProxyModel(parent=self.table)
|
||||||
|
self.proxy.setSourceModel(self.model)
|
||||||
|
self.table.setModel(self.proxy)
|
||||||
|
self.table.setSortingEnabled(True)
|
||||||
|
|
||||||
|
# Delegates
|
||||||
|
self.checkbox_delegate = CenterCheckBoxDelegate(self.table)
|
||||||
|
self.wrap_delegate = WrappingTextDelegate(self.table)
|
||||||
|
self.tool_tip_delegate = DictToolTipDelegate(self.table)
|
||||||
|
self.table.setItemDelegateForColumn(0, self.tool_tip_delegate) # name
|
||||||
|
self.table.setItemDelegateForColumn(1, self.tool_tip_delegate) # deviceClass
|
||||||
|
self.table.setItemDelegateForColumn(2, self.tool_tip_delegate) # readoutPriority
|
||||||
|
self.table.setItemDelegateForColumn(3, self.checkbox_delegate) # enabled
|
||||||
|
self.table.setItemDelegateForColumn(4, self.checkbox_delegate) # readOnly
|
||||||
|
self.table.setItemDelegateForColumn(5, self.wrap_delegate) # deviceTags
|
||||||
|
self.table.setItemDelegateForColumn(6, self.wrap_delegate) # description
|
||||||
|
|
||||||
|
# Column resize policies
|
||||||
|
# TODO maybe we need here a flexible header options as deviceClass
|
||||||
|
# may get quite long for beamlines plugin repos
|
||||||
|
header = self.table.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) # name
|
||||||
|
header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) # deviceClass
|
||||||
|
header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) # readoutPriority
|
||||||
|
header.setSectionResizeMode(3, QtWidgets.QHeaderView.Fixed) # enabled
|
||||||
|
header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) # readOnly
|
||||||
|
# TODO maybe better stretch...
|
||||||
|
header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeToContents) # deviceTags
|
||||||
|
header.setSectionResizeMode(6, QtWidgets.QHeaderView.Stretch) # description
|
||||||
|
self.table.setColumnWidth(3, 82)
|
||||||
|
self.table.setColumnWidth(4, 82)
|
||||||
|
|
||||||
|
# Ensure column widths stay fixed
|
||||||
|
header.setMinimumSectionSize(70)
|
||||||
|
header.setDefaultSectionSize(90)
|
||||||
|
|
||||||
|
# Enable resizing of column
|
||||||
|
header.sectionResized.connect(self.on_table_resized)
|
||||||
|
|
||||||
|
# Selection behavior
|
||||||
|
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||||
|
self.table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||||
|
self.table.horizontalHeader().setHighlightSections(False)
|
||||||
|
|
||||||
|
# QtCore.QTimer.singleShot(0, lambda: header.sectionResized.emit(0, 0, 0))
|
||||||
|
|
||||||
|
def device_config(self) -> list[dict]:
|
||||||
|
"""Get the device config."""
|
||||||
|
return self.model.get_device_config()
|
||||||
|
|
||||||
|
def apply_theme(self, theme: str | None = None):
|
||||||
|
self.checkbox_delegate.apply_theme(theme)
|
||||||
|
|
||||||
|
######################################
|
||||||
|
########### Slot API #################
|
||||||
|
######################################
|
||||||
|
|
||||||
|
@SafeSlot(int, int, int)
|
||||||
|
def on_table_resized(self, column, old_width, new_width):
|
||||||
|
"""Handle changes to the table column resizing."""
|
||||||
|
if column != len(self.model.headers) - 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in range(self.table.model().rowCount()):
|
||||||
|
index = self.table.model().index(row, column)
|
||||||
|
delegate = self.table.itemDelegate(index)
|
||||||
|
option = QtWidgets.QStyleOptionViewItem()
|
||||||
|
height = delegate.sizeHint(option, index).height()
|
||||||
|
self.table.setRowHeight(row, height)
|
||||||
|
|
||||||
|
######################################
|
||||||
|
##### Ext. Slot API #################
|
||||||
|
######################################
|
||||||
|
|
||||||
|
@SafeSlot(list)
|
||||||
|
def set_device_config(self, config: list[dict]):
|
||||||
|
"""
|
||||||
|
Set the device config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (list[dict]): The device config to set.
|
||||||
|
"""
|
||||||
|
self.model.set_device_config(config)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def clear_device_config(self):
|
||||||
|
"""
|
||||||
|
Clear the device config.
|
||||||
|
"""
|
||||||
|
self.model.set_device_config([])
|
||||||
|
|
||||||
|
@SafeSlot(dict)
|
||||||
|
def add_device(self, device: dict):
|
||||||
|
"""
|
||||||
|
Add a device to the config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device (dict): The device to add.
|
||||||
|
"""
|
||||||
|
self.model.add_device(device)
|
||||||
|
|
||||||
|
@SafeSlot(int)
|
||||||
|
@SafeSlot(str)
|
||||||
|
def remove_device(self, dev: int | str):
|
||||||
|
"""
|
||||||
|
Remove the device from the config either by row id, or device name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dev (int | str): The device to remove, either by row id or device name.
|
||||||
|
"""
|
||||||
|
if isinstance(dev, int):
|
||||||
|
# TODO test this properly, check with proxy index and source index
|
||||||
|
# Use the proxy model to map to the correct row
|
||||||
|
model_source_index = self.table.model().mapToSource(self.table.model().index(dev, 0))
|
||||||
|
self.model.remove_device_by_row(model_source_index.row())
|
||||||
|
return
|
||||||
|
if isinstance(dev, str):
|
||||||
|
self.model.remove_device_by_name(dev)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = DeviceTableView()
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
config = window.client.device_manager._get_redis_device_config()
|
||||||
|
window.set_device_config(config)
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
This module provides an implementation for the device config view.
|
||||||
|
The widget is the entry point for users to edit device configurations.
|
||||||
|
"""
|
||||||
@@ -45,6 +45,7 @@ class ScanControl(BECWidget, QWidget):
|
|||||||
Widget to submit new scans to the queue.
|
Widget to submit new scans to the queue.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
USER_ACCESS = ["remove", "screenshot"]
|
||||||
PLUGIN = True
|
PLUGIN = True
|
||||||
ICON_NAME = "tune"
|
ICON_NAME = "tune"
|
||||||
ARG_BOX_POSITION: int = 2
|
ARG_BOX_POSITION: int = 2
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
|
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
|
||||||
@@ -20,6 +21,8 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = ScanControl(parent)
|
t = ScanControl(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "Device Control"
|
return "BEC Device Control"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(ScanControl.ICON_NAME)
|
return designer_material_icon(ScanControl.ICON_NAME)
|
||||||
@@ -48,7 +51,7 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "ScanControl"
|
return "ScanControl"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "ScanControl"
|
return ""
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Literal
|
from typing import Literal, Sequence
|
||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from bec_qthemes import material_icon
|
from bec_qthemes import material_icon
|
||||||
@@ -36,7 +36,7 @@ class ScanArgType:
|
|||||||
BOOL = "bool"
|
BOOL = "bool"
|
||||||
STR = "str"
|
STR = "str"
|
||||||
DEVICEBASE = "DeviceBase"
|
DEVICEBASE = "DeviceBase"
|
||||||
LITERALS = "dict"
|
LITERALS_DICT = "dict" # Used when the type is provided as a dict with Literal key
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(QDialog):
|
class SettingsDialog(QDialog):
|
||||||
@@ -83,6 +83,39 @@ class ScanSpinBox(QSpinBox):
|
|||||||
self.setValue(default)
|
self.setValue(default)
|
||||||
|
|
||||||
|
|
||||||
|
class ScanLiteralsComboBox(QComboBox):
|
||||||
|
def __init__(
|
||||||
|
self, parent=None, arg_name: str | None = None, default: str | None = None, *args, **kwargs
|
||||||
|
):
|
||||||
|
super().__init__(parent=parent, *args, **kwargs)
|
||||||
|
self.arg_name = arg_name
|
||||||
|
self.default = default
|
||||||
|
if default is not None:
|
||||||
|
self.setCurrentText(default)
|
||||||
|
|
||||||
|
def set_literals(self, literals: Sequence[str | int | float | None]) -> None:
|
||||||
|
"""
|
||||||
|
Set the list of literals for the combo box.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
literals: List of literal values (can be strings, integers, floats or None)
|
||||||
|
"""
|
||||||
|
self.clear()
|
||||||
|
literals = set(literals) # Remove duplicates
|
||||||
|
if None in literals:
|
||||||
|
literals.remove(None)
|
||||||
|
self.addItem("")
|
||||||
|
|
||||||
|
self.addItems([str(value) for value in literals])
|
||||||
|
|
||||||
|
# find index of the default value
|
||||||
|
index = max(self.findText(str(self.default)), 0)
|
||||||
|
self.setCurrentIndex(index)
|
||||||
|
|
||||||
|
def get_value(self) -> str | None:
|
||||||
|
return self.currentText() if self.currentText() else None
|
||||||
|
|
||||||
|
|
||||||
class ScanDoubleSpinBox(QDoubleSpinBox):
|
class ScanDoubleSpinBox(QDoubleSpinBox):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, parent=None, arg_name: str = None, default: float | None = None, *args, **kwargs
|
self, parent=None, arg_name: str = None, default: float | None = None, *args, **kwargs
|
||||||
@@ -137,7 +170,7 @@ class ScanGroupBox(QGroupBox):
|
|||||||
ScanArgType.INT: ScanSpinBox,
|
ScanArgType.INT: ScanSpinBox,
|
||||||
ScanArgType.BOOL: ScanCheckBox,
|
ScanArgType.BOOL: ScanCheckBox,
|
||||||
ScanArgType.STR: ScanLineEdit,
|
ScanArgType.STR: ScanLineEdit,
|
||||||
ScanArgType.LITERALS: QComboBox, # TODO figure out combobox logic
|
ScanArgType.LITERALS_DICT: ScanLiteralsComboBox,
|
||||||
}
|
}
|
||||||
|
|
||||||
device_selected = Signal(str)
|
device_selected = Signal(str)
|
||||||
@@ -226,7 +259,11 @@ class ScanGroupBox(QGroupBox):
|
|||||||
for column_index, item in enumerate(group_inputs):
|
for column_index, item in enumerate(group_inputs):
|
||||||
arg_name = item.get("name", None)
|
arg_name = item.get("name", None)
|
||||||
default = item.get("default", None)
|
default = item.get("default", None)
|
||||||
widget_class = self.WIDGET_HANDLER.get(item["type"], None)
|
item_type = item.get("type", None)
|
||||||
|
if isinstance(item_type, dict) and "Literal" in item_type:
|
||||||
|
widget_class = self.WIDGET_HANDLER.get(ScanArgType.LITERALS_DICT, None)
|
||||||
|
else:
|
||||||
|
widget_class = self.WIDGET_HANDLER.get(item["type"], None)
|
||||||
if widget_class is None:
|
if widget_class is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Unsupported annotation '{item['type']}' for parameter '{item['name']}'"
|
f"Unsupported annotation '{item['type']}' for parameter '{item['name']}'"
|
||||||
@@ -239,6 +276,8 @@ class ScanGroupBox(QGroupBox):
|
|||||||
widget.set_device_filter(BECDeviceFilter.DEVICE)
|
widget.set_device_filter(BECDeviceFilter.DEVICE)
|
||||||
self.selected_devices[widget] = ""
|
self.selected_devices[widget] = ""
|
||||||
widget.device_selected.connect(self.emit_device_selected)
|
widget.device_selected.connect(self.emit_device_selected)
|
||||||
|
if isinstance(widget, ScanLiteralsComboBox):
|
||||||
|
widget.set_literals(item["type"].get("Literal", []))
|
||||||
tooltip = item.get("tooltip", None)
|
tooltip = item.get("tooltip", None)
|
||||||
if tooltip is not None:
|
if tooltip is not None:
|
||||||
widget.setToolTip(item["tooltip"])
|
widget.setToolTip(item["tooltip"])
|
||||||
@@ -336,6 +375,8 @@ class ScanGroupBox(QGroupBox):
|
|||||||
widget = self.layout.itemAtPosition(1, i).widget()
|
widget = self.layout.itemAtPosition(1, i).widget()
|
||||||
if isinstance(widget, DeviceLineEdit) and device_object:
|
if isinstance(widget, DeviceLineEdit) and device_object:
|
||||||
value = widget.get_current_device().name
|
value = widget.get_current_device().name
|
||||||
|
elif isinstance(widget, ScanLiteralsComboBox):
|
||||||
|
value = widget.get_value()
|
||||||
else:
|
else:
|
||||||
value = WidgetIO.get_value(widget)
|
value = WidgetIO.get_value(widget)
|
||||||
kwargs[widget.arg_name] = value
|
kwargs[widget.arg_name] = value
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||||
@@ -20,6 +21,8 @@ class DapComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = DapComboBox(parent)
|
t = DapComboBox(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
||||||
@@ -20,6 +21,8 @@ class LMFitDialogPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = LMFitDialog(parent)
|
t = LMFitDialog(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ class LMFitDialogPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "LMFitDialog"
|
return "LMFitDialog"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "LMFitDialog"
|
return "Dialog for displaying the fit summary and params for LMFit DAP processes"
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
import qtmonaco
|
import qtmonaco
|
||||||
|
from qtpy.QtCore import Signal
|
||||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
@@ -12,11 +13,14 @@ class MonacoWidget(BECWidget, QWidget):
|
|||||||
A simple Monaco editor widget
|
A simple Monaco editor widget
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
text_changed = Signal(str)
|
||||||
PLUGIN = True
|
PLUGIN = True
|
||||||
ICON_NAME = "code"
|
ICON_NAME = "code"
|
||||||
USER_ACCESS = [
|
USER_ACCESS = [
|
||||||
"set_text",
|
"set_text",
|
||||||
"get_text",
|
"get_text",
|
||||||
|
"insert_text",
|
||||||
|
"delete_line",
|
||||||
"set_language",
|
"set_language",
|
||||||
"get_language",
|
"get_language",
|
||||||
"set_theme",
|
"set_theme",
|
||||||
@@ -25,6 +29,9 @@ class MonacoWidget(BECWidget, QWidget):
|
|||||||
"set_cursor",
|
"set_cursor",
|
||||||
"current_cursor",
|
"current_cursor",
|
||||||
"set_minimap_enabled",
|
"set_minimap_enabled",
|
||||||
|
"set_vim_mode_enabled",
|
||||||
|
"set_lsp_header",
|
||||||
|
"get_lsp_header",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
||||||
@@ -36,6 +43,7 @@ class MonacoWidget(BECWidget, QWidget):
|
|||||||
self.editor = qtmonaco.Monaco(self)
|
self.editor = qtmonaco.Monaco(self)
|
||||||
layout.addWidget(self.editor)
|
layout.addWidget(self.editor)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
self.editor.text_changed.connect(self.text_changed.emit)
|
||||||
self.editor.initialized.connect(self.apply_theme)
|
self.editor.initialized.connect(self.apply_theme)
|
||||||
|
|
||||||
def apply_theme(self, theme: str | None = None) -> None:
|
def apply_theme(self, theme: str | None = None) -> None:
|
||||||
@@ -65,6 +73,26 @@ class MonacoWidget(BECWidget, QWidget):
|
|||||||
"""
|
"""
|
||||||
return self.editor.get_text()
|
return self.editor.get_text()
|
||||||
|
|
||||||
|
def insert_text(self, text: str, line: int | None = None, column: int | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Insert text at the current cursor position or at a specified line and column.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The text to insert.
|
||||||
|
line (int, optional): The line number (1-based) to insert the text at. Defaults to None.
|
||||||
|
column (int, optional): The column number (1-based) to insert the text at. Defaults to None.
|
||||||
|
"""
|
||||||
|
self.editor.insert_text(text, line, column)
|
||||||
|
|
||||||
|
def delete_line(self, line: int | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Delete a line in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line (int, optional): The line number (1-based) to delete. If None, the current line will be deleted.
|
||||||
|
"""
|
||||||
|
self.editor.delete_line(line)
|
||||||
|
|
||||||
def set_cursor(
|
def set_cursor(
|
||||||
self,
|
self,
|
||||||
line: int,
|
line: int,
|
||||||
@@ -154,6 +182,34 @@ class MonacoWidget(BECWidget, QWidget):
|
|||||||
"""
|
"""
|
||||||
self.editor.clear_highlighted_lines()
|
self.editor.clear_highlighted_lines()
|
||||||
|
|
||||||
|
def set_vim_mode_enabled(self, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
Enable or disable Vim mode in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled (bool): If True, Vim mode will be enabled; otherwise, it will be disabled.
|
||||||
|
"""
|
||||||
|
self.editor.set_vim_mode_enabled(enabled)
|
||||||
|
|
||||||
|
def set_lsp_header(self, header: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the LSP (Language Server Protocol) header for the Monaco editor.
|
||||||
|
The header is used to provide context for language servers but is not displayed in the editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
header (str): The LSP header to set.
|
||||||
|
"""
|
||||||
|
self.editor.set_lsp_header(header)
|
||||||
|
|
||||||
|
def get_lsp_header(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the current LSP header set in the Monaco editor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The LSP header.
|
||||||
|
"""
|
||||||
|
return self.editor.get_lsp_header()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
qapp = QApplication([])
|
qapp = QApplication([])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||||
@@ -20,6 +21,8 @@ class MonacoWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = MonacoWidget(parent)
|
t = MonacoWidget(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.editors.sbb_monitor.sbb_monitor import SBBMonitor
|
from bec_widgets.widgets.editors.sbb_monitor.sbb_monitor import SBBMonitor
|
||||||
@@ -20,6 +21,8 @@ class SBBMonitorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = SBBMonitor(parent)
|
t = SBBMonitor(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ class SBBMonitorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return ""
|
return "BEC Utils"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(SBBMonitor.ICON_NAME)
|
return designer_material_icon(SBBMonitor.ICON_NAME)
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ class ScanMetadata(PydanticModelForm):
|
|||||||
def set_schema_from_scan(self, scan_name: str | None):
|
def set_schema_from_scan(self, scan_name: str | None):
|
||||||
self._scan_name = scan_name or ""
|
self._scan_name = scan_name or ""
|
||||||
self.set_schema(get_metadata_schema_for_scan(self._scan_name))
|
self.set_schema(get_metadata_schema_for_scan(self._scan_name))
|
||||||
self.populate()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
||||||
@@ -20,6 +21,8 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = ScanMetadata(parent)
|
t = ScanMetadata(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return ""
|
return "BEC Input Widgets"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(ScanMetadata.ICON_NAME)
|
return designer_material_icon(ScanMetadata.ICON_NAME)
|
||||||
@@ -48,7 +51,7 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "ScanMetadata"
|
return "ScanMetadata"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "Dynamically generates a form for inclusion of metadata for a scan."
|
return "ScanMetadata"
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
import bec_widgets
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.editors.text_box.text_box import TextBox
|
from bec_widgets.widgets.editors.text_box.text_box import TextBox
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ DOM_XML = """
|
|||||||
</widget>
|
</widget>
|
||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
@@ -23,6 +21,8 @@ class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = TextBox(parent)
|
t = TextBox(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "TextBox"
|
return "TextBox"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "TextBox"
|
return "A widget that displays text in plain and HTML format"
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
import bec_widgets
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
|
from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
|
||||||
|
|
||||||
@@ -15,8 +14,6 @@ DOM_XML = """
|
|||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -24,6 +21,8 @@ class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = VSCodeEditor(parent)
|
t = VSCodeEditor(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import time
|
|||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from louie.saferef import safe_ref
|
from louie.saferef import safe_ref
|
||||||
from qtpy.QtCore import QUrl, qInstallMessageHandler
|
from qtpy.QtCore import QTimer, QUrl, Signal, qInstallMessageHandler
|
||||||
from qtpy.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
|
from qtpy.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
|
||||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.error_popups import SafeProperty
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@@ -165,11 +166,16 @@ class WebConsole(BECWidget, QWidget):
|
|||||||
A simple widget to display a website
|
A simple widget to display a website
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_js_callback = Signal(bool)
|
||||||
|
initialized = Signal()
|
||||||
|
|
||||||
PLUGIN = True
|
PLUGIN = True
|
||||||
ICON_NAME = "terminal"
|
ICON_NAME = "terminal"
|
||||||
|
|
||||||
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
||||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||||
|
self._startup_cmd = "bec --nogui"
|
||||||
|
self._is_initialized = False
|
||||||
_web_console_registry.register(self)
|
_web_console_registry.register(self)
|
||||||
self._token = _web_console_registry._token
|
self._token = _web_console_registry._token
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
@@ -181,6 +187,48 @@ class WebConsole(BECWidget, QWidget):
|
|||||||
layout.addWidget(self.browser)
|
layout.addWidget(self.browser)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
self.page.setUrl(QUrl(f"http://localhost:{_web_console_registry._server_port}"))
|
self.page.setUrl(QUrl(f"http://localhost:{_web_console_registry._server_port}"))
|
||||||
|
self._startup_timer = QTimer()
|
||||||
|
self._startup_timer.setInterval(500)
|
||||||
|
self._startup_timer.timeout.connect(self._check_page_ready)
|
||||||
|
self._startup_timer.start()
|
||||||
|
self._js_callback.connect(self._on_js_callback)
|
||||||
|
|
||||||
|
def _check_page_ready(self):
|
||||||
|
"""
|
||||||
|
Check if the page is ready and stop the timer if it is.
|
||||||
|
"""
|
||||||
|
if self.page.isLoading():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.page.runJavaScript("window.term !== undefined", self._js_callback.emit)
|
||||||
|
|
||||||
|
def _on_js_callback(self, ready: bool):
|
||||||
|
"""
|
||||||
|
Callback for when the JavaScript is ready.
|
||||||
|
"""
|
||||||
|
if not ready:
|
||||||
|
return
|
||||||
|
self._is_initialized = True
|
||||||
|
self._startup_timer.stop()
|
||||||
|
if self._startup_cmd:
|
||||||
|
self.write(self._startup_cmd)
|
||||||
|
self.initialized.emit()
|
||||||
|
|
||||||
|
@SafeProperty(str)
|
||||||
|
def startup_cmd(self):
|
||||||
|
"""
|
||||||
|
Get the startup command for the web console.
|
||||||
|
"""
|
||||||
|
return self._startup_cmd
|
||||||
|
|
||||||
|
@startup_cmd.setter
|
||||||
|
def startup_cmd(self, cmd: str):
|
||||||
|
"""
|
||||||
|
Set the startup command for the web console.
|
||||||
|
"""
|
||||||
|
if not isinstance(cmd, str):
|
||||||
|
raise ValueError("Startup command must be a string.")
|
||||||
|
self._startup_cmd = cmd
|
||||||
|
|
||||||
def write(self, data: str, send_return: bool = True):
|
def write(self, data: str, send_return: bool = True):
|
||||||
"""
|
"""
|
||||||
@@ -213,10 +261,19 @@ class WebConsole(BECWidget, QWidget):
|
|||||||
"document.querySelector('textarea.xterm-helper-textarea').dispatchEvent(new KeyboardEvent('keypress', {charCode: 3}))"
|
"document.querySelector('textarea.xterm-helper-textarea').dispatchEvent(new KeyboardEvent('keypress', {charCode: 3}))"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_readonly(self, readonly: bool):
|
||||||
|
"""
|
||||||
|
Set the web console to read-only mode.
|
||||||
|
"""
|
||||||
|
if not isinstance(readonly, bool):
|
||||||
|
raise ValueError("Readonly must be a boolean.")
|
||||||
|
self.setEnabled(not readonly)
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""
|
"""
|
||||||
Clean up the registry by removing any instances that are no longer valid.
|
Clean up the registry by removing any instances that are no longer valid.
|
||||||
"""
|
"""
|
||||||
|
self._startup_timer.stop()
|
||||||
_web_console_registry.unregister(self)
|
_web_console_registry.unregister(self)
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.editors.web_console.web_console import WebConsole
|
from bec_widgets.widgets.editors.web_console.web_console import WebConsole
|
||||||
@@ -20,6 +21,8 @@ class WebConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = WebConsole(parent)
|
t = WebConsole(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ class WebConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "BEC Console"
|
return "BEC Developer"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(WebConsole.ICON_NAME)
|
return designer_material_icon(WebConsole.ICON_NAME)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# Copyright (C) 2022 The Qt Company Ltd.
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
import os
|
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
import bec_widgets
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.editors.website.website import WebsiteWidget
|
from bec_widgets.widgets.editors.website.website import WebsiteWidget
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ DOM_XML = """
|
|||||||
</widget>
|
</widget>
|
||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class WebsiteWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class WebsiteWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
@@ -23,6 +21,8 @@ class WebsiteWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = WebsiteWidget(parent)
|
t = WebsiteWidget(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.games.minesweeper import Minesweeper
|
from bec_widgets.widgets.games.minesweeper import Minesweeper
|
||||||
@@ -20,6 +21,8 @@ class MinesweeperPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = Minesweeper(parent)
|
t = Minesweeper(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
|||||||
from bec_widgets.widgets.plots.heatmap.settings.heatmap_setting import HeatmapSettings
|
from bec_widgets.widgets.plots.heatmap.settings.heatmap_setting import HeatmapSettings
|
||||||
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
||||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||||
|
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@@ -83,38 +84,7 @@ class Heatmap(ImageBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
USER_ACCESS = [
|
USER_ACCESS = [
|
||||||
# General PlotBase Settings
|
*PlotBase.USER_ACCESS,
|
||||||
"enable_toolbar",
|
|
||||||
"enable_toolbar.setter",
|
|
||||||
"enable_side_panel",
|
|
||||||
"enable_side_panel.setter",
|
|
||||||
"enable_fps_monitor",
|
|
||||||
"enable_fps_monitor.setter",
|
|
||||||
"set",
|
|
||||||
"title",
|
|
||||||
"title.setter",
|
|
||||||
"x_label",
|
|
||||||
"x_label.setter",
|
|
||||||
"y_label",
|
|
||||||
"y_label.setter",
|
|
||||||
"x_limits",
|
|
||||||
"x_limits.setter",
|
|
||||||
"y_limits",
|
|
||||||
"y_limits.setter",
|
|
||||||
"x_grid",
|
|
||||||
"x_grid.setter",
|
|
||||||
"y_grid",
|
|
||||||
"y_grid.setter",
|
|
||||||
"inner_axes",
|
|
||||||
"inner_axes.setter",
|
|
||||||
"outer_axes",
|
|
||||||
"outer_axes.setter",
|
|
||||||
"auto_range_x",
|
|
||||||
"auto_range_x.setter",
|
|
||||||
"auto_range_y",
|
|
||||||
"auto_range_y.setter",
|
|
||||||
"minimal_crosshair_precision",
|
|
||||||
"minimal_crosshair_precision.setter",
|
|
||||||
# ImageView Specific Settings
|
# ImageView Specific Settings
|
||||||
"color_map",
|
"color_map",
|
||||||
"color_map.setter",
|
"color_map.setter",
|
||||||
@@ -124,8 +94,6 @@ class Heatmap(ImageBase):
|
|||||||
"v_min.setter",
|
"v_min.setter",
|
||||||
"v_max",
|
"v_max",
|
||||||
"v_max.setter",
|
"v_max.setter",
|
||||||
"lock_aspect_ratio",
|
|
||||||
"lock_aspect_ratio.setter",
|
|
||||||
"autorange",
|
"autorange",
|
||||||
"autorange.setter",
|
"autorange.setter",
|
||||||
"autorange_mode",
|
"autorange_mode",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap
|
from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap
|
||||||
@@ -20,6 +21,8 @@ class HeatmapPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = Heatmap(parent)
|
t = Heatmap(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ class HeatmapPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "Plot Widgets"
|
return "BEC Plots"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(Heatmap.ICON_NAME)
|
return designer_material_icon(Heatmap.ICON_NAME)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Literal
|
from typing import Literal, Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
@@ -22,6 +22,7 @@ from bec_widgets.widgets.control.device_input.base_classes.device_input_base imp
|
|||||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||||
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
||||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||||
|
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@@ -59,38 +60,7 @@ class Image(ImageBase):
|
|||||||
RPC = True
|
RPC = True
|
||||||
ICON_NAME = "image"
|
ICON_NAME = "image"
|
||||||
USER_ACCESS = [
|
USER_ACCESS = [
|
||||||
# General PlotBase Settings
|
*PlotBase.USER_ACCESS,
|
||||||
"enable_toolbar",
|
|
||||||
"enable_toolbar.setter",
|
|
||||||
"enable_side_panel",
|
|
||||||
"enable_side_panel.setter",
|
|
||||||
"enable_fps_monitor",
|
|
||||||
"enable_fps_monitor.setter",
|
|
||||||
"set",
|
|
||||||
"title",
|
|
||||||
"title.setter",
|
|
||||||
"x_label",
|
|
||||||
"x_label.setter",
|
|
||||||
"y_label",
|
|
||||||
"y_label.setter",
|
|
||||||
"x_limits",
|
|
||||||
"x_limits.setter",
|
|
||||||
"y_limits",
|
|
||||||
"y_limits.setter",
|
|
||||||
"x_grid",
|
|
||||||
"x_grid.setter",
|
|
||||||
"y_grid",
|
|
||||||
"y_grid.setter",
|
|
||||||
"inner_axes",
|
|
||||||
"inner_axes.setter",
|
|
||||||
"outer_axes",
|
|
||||||
"outer_axes.setter",
|
|
||||||
"auto_range_x",
|
|
||||||
"auto_range_x.setter",
|
|
||||||
"auto_range_y",
|
|
||||||
"auto_range_y.setter",
|
|
||||||
"minimal_crosshair_precision",
|
|
||||||
"minimal_crosshair_precision.setter",
|
|
||||||
# ImageView Specific Settings
|
# ImageView Specific Settings
|
||||||
"color_map",
|
"color_map",
|
||||||
"color_map.setter",
|
"color_map.setter",
|
||||||
@@ -100,8 +70,6 @@ class Image(ImageBase):
|
|||||||
"v_min.setter",
|
"v_min.setter",
|
||||||
"v_max",
|
"v_max",
|
||||||
"v_max.setter",
|
"v_max.setter",
|
||||||
"lock_aspect_ratio",
|
|
||||||
"lock_aspect_ratio.setter",
|
|
||||||
"autorange",
|
"autorange",
|
||||||
"autorange.setter",
|
"autorange.setter",
|
||||||
"autorange_mode",
|
"autorange_mode",
|
||||||
@@ -306,7 +274,7 @@ class Image(ImageBase):
|
|||||||
Set the image source and update the image.
|
Set the image source and update the image.
|
||||||
|
|
||||||
Args:
|
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".
|
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_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".
|
color_bar(str): The type of color bar to use. Options are "simple" or "full".
|
||||||
@@ -321,10 +289,13 @@ class Image(ImageBase):
|
|||||||
if monitor is None or monitor == "":
|
if monitor is None or monitor == "":
|
||||||
logger.warning(f"No monitor specified, cannot set image, old monitor is unsubscribed")
|
logger.warning(f"No monitor specified, cannot set image, old monitor is unsubscribed")
|
||||||
return None
|
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])
|
self.entry_validator.validate_monitor(monitor[0])
|
||||||
else:
|
else:
|
||||||
self.entry_validator.validate_monitor(monitor)
|
raise ValueError(f"Invalid monitor type: {type(monitor)}")
|
||||||
|
|
||||||
self.set_image_update(monitor=monitor, type=monitor_type)
|
self.set_image_update(monitor=monitor, type=monitor_type)
|
||||||
if color_map is not None:
|
if color_map is not None:
|
||||||
@@ -346,7 +317,7 @@ class Image(ImageBase):
|
|||||||
if config.monitor is not None:
|
if config.monitor is not None:
|
||||||
for combo in (self.device_combo_box, self.dim_combo_box):
|
for combo in (self.device_combo_box, self.dim_combo_box):
|
||||||
combo.blockSignals(True)
|
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]}")
|
self.device_combo_box.setCurrentText(f"{config.monitor[0]}_{config.monitor[1]}")
|
||||||
else:
|
else:
|
||||||
self.device_combo_box.setCurrentText(config.monitor)
|
self.device_combo_box.setCurrentText(config.monitor)
|
||||||
@@ -451,7 +422,7 @@ class Image(ImageBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO consider moving connecting and disconnecting logic to Image itself if multiple images
|
# 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]]
|
device = self.dev[monitor[0]]
|
||||||
signal = monitor[1]
|
signal = monitor[1]
|
||||||
if len(monitor) == 3:
|
if len(monitor) == 3:
|
||||||
@@ -519,7 +490,7 @@ class Image(ImageBase):
|
|||||||
Args:
|
Args:
|
||||||
monitor(str|tuple): The name of the monitor to disconnect, or a tuple of (device, signal) for preview signals.
|
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":
|
if self.subscriptions["main"].source == "device_monitor_1d":
|
||||||
self.bec_dispatcher.disconnect_slot(
|
self.bec_dispatcher.disconnect_slot(
|
||||||
self.on_image_update_1d, MessageEndpoints.device_preview(monitor[0], monitor[1])
|
self.on_image_update_1d, MessageEndpoints.device_preview(monitor[0], monitor[1])
|
||||||
|
|||||||
@@ -1034,7 +1034,8 @@ class ImageBase(PlotBase):
|
|||||||
if self.y_roi is not None:
|
if self.y_roi is not None:
|
||||||
self.y_roi.cleanup_pyqtgraph()
|
self.y_roi.cleanup_pyqtgraph()
|
||||||
|
|
||||||
self.layer_manager.clear()
|
if self.layer_manager is not None:
|
||||||
self.layer_manager = None
|
self.layer_manager.clear()
|
||||||
|
self.layer_manager = None
|
||||||
|
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
from bec_widgets.widgets.plots.image.image import Image
|
from bec_widgets.widgets.plots.image.image import Image
|
||||||
@@ -20,6 +21,8 @@ class ImagePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
|
|
||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
|
if parent is None:
|
||||||
|
return QWidget()
|
||||||
t = Image(parent)
|
t = Image(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ class ImagePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "Plot Widgets"
|
return "BEC Plots"
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(Image.ICON_NAME)
|
return designer_material_icon(Image.ICON_NAME)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
from bec_qthemes import material_icon
|
from bec_qthemes import material_icon
|
||||||
@@ -73,11 +73,16 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
- Children: type, line-width (spin box), coordinates (auto-updating).
|
- Children: type, line-width (spin box), coordinates (auto-updating).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
parent (QWidget, optional): Parent widget. Defaults to None.
|
||||||
image_widget (Image): The main Image widget that displays the ImageItem.
|
image_widget (Image): The main Image widget that displays the ImageItem.
|
||||||
Provides ``plot_item`` and owns an ROIController already.
|
Provides ``plot_item`` and owns an ROIController already.
|
||||||
controller (ROIController, optional): Optionally pass an external controller.
|
controller (ROIController, optional): Optionally pass an external controller.
|
||||||
If None, the manager uses ``image_widget.roi_controller``.
|
If None, the manager uses ``image_widget.roi_controller``.
|
||||||
parent (QWidget, optional): Parent widget. Defaults to None.
|
compact (bool, optional): If True, use a compact mode with no tree view,
|
||||||
|
only a toolbar with draw actions. Defaults to False.
|
||||||
|
compact_orientation (str, optional): Orientation of the toolbar in compact mode.
|
||||||
|
Either "vertical" or "horizontal". Defaults to "vertical".
|
||||||
|
compact_color (str, optional): Color of the single active ROI in compact mode.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PLUGIN = False
|
PLUGIN = False
|
||||||
@@ -92,11 +97,18 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
parent: QWidget = None,
|
parent: QWidget = None,
|
||||||
image_widget: Image,
|
image_widget: Image,
|
||||||
controller: ROIController | None = None,
|
controller: ROIController | None = None,
|
||||||
|
compact: bool = False,
|
||||||
|
compact_orientation: Literal["vertical", "horizontal"] = "vertical",
|
||||||
|
compact_color: str = "#f0f0f0",
|
||||||
):
|
):
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
parent=parent, config=ConnectionConfig(widget_class=self.__class__.__name__)
|
parent=parent, config=ConnectionConfig(widget_class=self.__class__.__name__)
|
||||||
)
|
)
|
||||||
|
self.compact = compact
|
||||||
|
self.compact_orient = compact_orientation
|
||||||
|
self.compact_color = compact_color
|
||||||
|
self.single_active_roi: BaseROI | None = None
|
||||||
|
|
||||||
if controller is None:
|
if controller is None:
|
||||||
# Use the controller already belonging to the Image widget
|
# Use the controller already belonging to the Image widget
|
||||||
@@ -112,22 +124,29 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
|
|
||||||
self.layout = QVBoxLayout(self)
|
self.layout = QVBoxLayout(self)
|
||||||
self._init_toolbar()
|
self._init_toolbar()
|
||||||
self._init_tree()
|
if not self.compact:
|
||||||
|
self._init_tree()
|
||||||
|
else:
|
||||||
|
self.tree = None
|
||||||
|
|
||||||
# connect controller
|
# connect controller
|
||||||
self.controller.roiAdded.connect(self._on_roi_added)
|
self.controller.roiAdded.connect(self._on_roi_added)
|
||||||
self.controller.roiRemoved.connect(self._on_roi_removed)
|
self.controller.roiRemoved.connect(self._on_roi_removed)
|
||||||
self.controller.cleared.connect(self.tree.clear)
|
if not self.compact:
|
||||||
|
self.controller.cleared.connect(self.tree.clear)
|
||||||
|
|
||||||
# initial load
|
# initial load
|
||||||
for r in self.controller.rois:
|
for r in self.controller.rois:
|
||||||
self._on_roi_added(r)
|
self._on_roi_added(r)
|
||||||
|
|
||||||
self.tree.collapseAll()
|
if not self.compact:
|
||||||
|
self.tree.collapseAll()
|
||||||
|
|
||||||
# --------------------------------------------------------------------- UI
|
# --------------------------------------------------------------------- UI
|
||||||
def _init_toolbar(self):
|
def _init_toolbar(self):
|
||||||
tb = self.toolbar = ModularToolBar(self, orientation="horizontal")
|
tb = self.toolbar = ModularToolBar(
|
||||||
|
self, orientation=self.compact_orient if self.compact else "horizontal"
|
||||||
|
)
|
||||||
self._draw_actions: dict[str, MaterialIconAction] = {}
|
self._draw_actions: dict[str, MaterialIconAction] = {}
|
||||||
# --- ROI draw actions (toggleable) ---
|
# --- ROI draw actions (toggleable) ---
|
||||||
|
|
||||||
@@ -157,6 +176,29 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
for mode, act in self._draw_actions.items():
|
for mode, act in self._draw_actions.items():
|
||||||
act.action.toggled.connect(lambda on, m=mode: self._on_draw_action_toggled(m, on))
|
act.action.toggled.connect(lambda on, m=mode: self._on_draw_action_toggled(m, on))
|
||||||
|
|
||||||
|
if self.compact:
|
||||||
|
tb.components.add_safe(
|
||||||
|
"compact_delete",
|
||||||
|
MaterialIconAction("delete", "Delete Current Roi", checkable=False, parent=self),
|
||||||
|
)
|
||||||
|
bundle.add_action("compact_delete")
|
||||||
|
tb.components.get_action("compact_delete").action.triggered.connect(
|
||||||
|
lambda _: (
|
||||||
|
self.controller.remove_roi(self.single_active_roi)
|
||||||
|
if self.single_active_roi is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tb.show_bundles(["roi_draw"])
|
||||||
|
self.layout.addWidget(tb)
|
||||||
|
|
||||||
|
# ROI drawing state (needed even in compact mode)
|
||||||
|
self._roi_draw_mode = None
|
||||||
|
self._roi_start_pos = None
|
||||||
|
self._temp_roi = None
|
||||||
|
self.plot.scene().installEventFilter(self)
|
||||||
|
return
|
||||||
|
|
||||||
# Expand/Collapse toggle
|
# Expand/Collapse toggle
|
||||||
self.expand_toggle = MaterialIconAction(
|
self.expand_toggle = MaterialIconAction(
|
||||||
"unfold_more", "Expand/Collapse", checkable=True, parent=self # icon when collapsed
|
"unfold_more", "Expand/Collapse", checkable=True, parent=self # icon when collapsed
|
||||||
@@ -327,13 +369,21 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
self._set_roi_draw_mode(None)
|
self._set_roi_draw_mode(None)
|
||||||
# register via controller
|
# register via controller
|
||||||
self.controller.add_roi(final_roi)
|
self.controller.add_roi(final_roi)
|
||||||
|
if self.compact:
|
||||||
|
final_roi.line_color = self.compact_color
|
||||||
return True
|
return True
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
# --------------------------------------------------------- controller slots
|
# --------------------------------------------------------- controller slots
|
||||||
def _on_roi_added(self, roi: BaseROI):
|
def _on_roi_added(self, roi: BaseROI):
|
||||||
|
if self.compact:
|
||||||
|
roi.line_color = self.compact_color
|
||||||
|
if self.single_active_roi is not None and self.single_active_roi is not roi:
|
||||||
|
self.controller.remove_roi(self.single_active_roi)
|
||||||
|
self.single_active_roi = roi
|
||||||
|
return
|
||||||
# check the global setting from the toolbar
|
# check the global setting from the toolbar
|
||||||
if self.lock_all_action.action.isChecked():
|
if hasattr(self, "lock_all_action") and self.lock_all_action.action.isChecked():
|
||||||
roi.movable = False
|
roi.movable = False
|
||||||
# parent row with blank action column, name in ROI column
|
# parent row with blank action column, name in ROI column
|
||||||
parent = QTreeWidgetItem(self.tree, ["", "", ""])
|
parent = QTreeWidgetItem(self.tree, ["", "", ""])
|
||||||
@@ -424,6 +474,10 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
roi.movable = not roi.movable
|
roi.movable = not roi.movable
|
||||||
|
|
||||||
def _on_roi_removed(self, roi: BaseROI):
|
def _on_roi_removed(self, roi: BaseROI):
|
||||||
|
if self.compact:
|
||||||
|
if self.single_active_roi is roi:
|
||||||
|
self.single_active_roi = None
|
||||||
|
return
|
||||||
item = self.roi_items.pop(roi, None)
|
item = self.roi_items.pop(roi, None)
|
||||||
if item:
|
if item:
|
||||||
idx = self.tree.indexOfTopLevelItem(item)
|
idx = self.tree.indexOfTopLevelItem(item)
|
||||||
@@ -449,8 +503,9 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
self.controller.remove_roi(roi)
|
self.controller.remove_roi(roi)
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.cmap.close()
|
if hasattr(self, "cmap"):
|
||||||
self.cmap.deleteLater()
|
self.cmap.close()
|
||||||
|
self.cmap.deleteLater()
|
||||||
if self.controller and hasattr(self.controller, "rois"):
|
if self.controller and hasattr(self.controller, "rois"):
|
||||||
for roi in self.controller.rois: # disconnect all signals from ROIs
|
for roi in self.controller.rois: # disconnect all signals from ROIs
|
||||||
try:
|
try:
|
||||||
@@ -491,8 +546,8 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
# Add the image widget on the left
|
# Add the image widget on the left
|
||||||
ml.addWidget(image_widget)
|
ml.addWidget(image_widget)
|
||||||
|
|
||||||
# ROI manager linked to that image
|
# ROI manager linked to that image with compact mode
|
||||||
mgr = ROIPropertyTree(parent=image_widget, image_widget=image_widget)
|
mgr = ROIPropertyTree(parent=image_widget, image_widget=image_widget, compact=True)
|
||||||
mgr.setFixedWidth(350)
|
mgr.setFixedWidth(350)
|
||||||
ml.addWidget(mgr)
|
ml.addWidget(mgr)
|
||||||
|
|
||||||
|
|||||||
@@ -90,44 +90,7 @@ class MotorMap(PlotBase):
|
|||||||
RPC = True
|
RPC = True
|
||||||
ICON_NAME = "my_location"
|
ICON_NAME = "my_location"
|
||||||
USER_ACCESS = [
|
USER_ACCESS = [
|
||||||
# General PlotBase Settings
|
*PlotBase.USER_ACCESS,
|
||||||
"enable_toolbar",
|
|
||||||
"enable_toolbar.setter",
|
|
||||||
"enable_side_panel",
|
|
||||||
"enable_side_panel.setter",
|
|
||||||
"enable_fps_monitor",
|
|
||||||
"enable_fps_monitor.setter",
|
|
||||||
"set",
|
|
||||||
"title",
|
|
||||||
"title.setter",
|
|
||||||
"x_label",
|
|
||||||
"x_label.setter",
|
|
||||||
"y_label",
|
|
||||||
"y_label.setter",
|
|
||||||
"x_limits",
|
|
||||||
"x_limits.setter",
|
|
||||||
"y_limits",
|
|
||||||
"y_limits.setter",
|
|
||||||
"x_grid",
|
|
||||||
"x_grid.setter",
|
|
||||||
"y_grid",
|
|
||||||
"y_grid.setter",
|
|
||||||
"inner_axes",
|
|
||||||
"inner_axes.setter",
|
|
||||||
"outer_axes",
|
|
||||||
"outer_axes.setter",
|
|
||||||
"lock_aspect_ratio",
|
|
||||||
"lock_aspect_ratio.setter",
|
|
||||||
"auto_range_x",
|
|
||||||
"auto_range_x.setter",
|
|
||||||
"auto_range_y",
|
|
||||||
"auto_range_y.setter",
|
|
||||||
"x_log",
|
|
||||||
"x_log.setter",
|
|
||||||
"y_log",
|
|
||||||
"y_log.setter",
|
|
||||||
"legend_label_size",
|
|
||||||
"legend_label_size.setter",
|
|
||||||
# motor_map specific
|
# motor_map specific
|
||||||
"color",
|
"color",
|
||||||
"color.setter",
|
"color.setter",
|
||||||
@@ -764,7 +727,7 @@ class MotorMap(PlotBase):
|
|||||||
float: Motor initial position.
|
float: Motor initial position.
|
||||||
"""
|
"""
|
||||||
entry = self.entry_validator.validate_signal(name, None)
|
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
|
return init_position
|
||||||
|
|
||||||
def _sync_motor_map_selection_toolbar(self):
|
def _sync_motor_map_selection_toolbar(self):
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user